I have 2 classes: TableA and TableB, corresponding to the SQLite tables in the first version of my App.
Now I am ready to release the next version, where I have to add another table, TableC.
In each class, I have an onCreate(SQLiteDatabase sqlDatabase) and onUpgrade(SQLiteDatabase sqlDatabase, int oldVersion, int newVersion) method.
Here is what I want to do: keep TableA as it is,
add a new column to TableB
and, of course, create a new TableC.
I have updated my Database version number in the SQLiteOpenHelper
I have my methods implemented as below:
TableA.onUpgrade(/*parameters*/){
//Nothing to do here
}
TableB.onUpgrade(/*parameters*/){
sqlDatabase.execSQL("ALTER TABLE TableB ADD COLUMN columnFoo INTEGER DEFAULT 1");
}
TableC.onUpgrade(/*parameters*/){
sqlDatabase.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(sqlDatabase); //calls sqlDatabase.execSQL(CREATE_TableC);
}
My SQLiteOpenHelper subclass implementation looks as below:
#Override
public void onCreate(SQLiteDatabase sqlDatabase) {
TableA.onCreate(sqlDatabase);
TableB.onCreate(sqlDatabase);
TableC.onCreate(sqlDatabase);
}
#Override
public void onUpgrade(SQLiteDatabase sqlDatabase, int oldVersion, int newVersion)
{
TableA.onUpgrade(sqlDatabase);
TableB.onUpgrade(sqlDatabase);
TableC.onUpgrade(sqlDatabase);
}
My problem is that I am having trouble testing this.
When I installed the new version of my app on a fresh Genymotion Android VM, I got an error on a query which I do in my Launch activity on TableB, saying that the new column does not exist.
I tested by upgrading on an existing Genymotion Android VM which had version 1 of my app installed.
There it seemed to work fine.
I certainly can go on creating new VM's for every run, and installing my app to test.
But I wanted to ask what I could be doing wrong, and what would be a good way to test.
How do I correctly handle updating the database for my existing users, and how do I create new database/tables for my new users?
Any help is appreciated!!
My App supports Android API 14 (ICS) and above.
Problem happens when device #1 have app db version lets say 2, device #2 have app db version 5, and new release have db version 6.
Then ideally:
device 1 should start upgrade from 2->3->4->5->6.
device 2: 5->6.
So old upgrade code stay intact, and you keep adding new upgrade code for new version.
Here is an example (one of the way to handle onUpgrade()):
#Override
public void onUpgrade(SQLiteDatabase sqlDatabase, int oldVersion, int newVersion) {
try {
if (oldVersion < 51) {
upgradeToVersion51(db); // From 50 or 51
oldVersion = 51;
}
if (oldVersion == 51) {
upgradeToVersion52(db);
oldVersion += 1;
}
if (oldVersion == 52) {
upgradeToVersion53(db);
oldVersion += 1;
}
} catch (SQLiteException e) {
Log.e(TAG, "onUpgrade: SQLiteException, recreating db. ", e);
Log.e(TAG, "(oldVersion was " + oldVersion + ")");
return; // this was lossy
}
}
Note: you need to bump static final int DATABASE_VERSION; whenever you upgrade, check this link for example.
Check how Calendar handles db upgrade here: http://grepcode.com/file_/repository.grepcode.com/java/ext/com.google.android/android-apps/4.0.1_r1/com/android/providers/calendar/CalendarDatabaseHelper.java/?v=source
Hope this helps!
Related
I'm trying to migrate a Room database from versions 2 and 3 to 4 like so:
private static final Migration MIGRATION_2_4 = new Migration(2, 4) {
#Override
public void migrate(#NonNull #NotNull SupportSQLiteDatabase database) {
database.execSQL(
"ALTER TABLE 'market_data' ADD COLUMN 'watch_list_boolean' TEXT NOT NULL DEFAULT '0'");
database.execSQL("DROP TABLE 'developer_data'");
}
};
but it's not working, what is wrong here?
The problem, most likely (post your stack trace to help future readers), is that your DB won't be able to perform migrations 2->3 and 3->4
So, your code will only work if your db is upgraded from 2 directly to 4 and will throw an exception (that indicates what migration is missing) if db is upgraded from 2 to 3 or from 3 to 4.
Best practice is to create separate migrations - 2 to 3, and 3 to 4.
Room will know to execute the correct migrations and in the right order (2->3 or 3->4 or 2->3->4):
private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
#Override
public void migrate(#NonNull #NotNull SupportSQLiteDatabase database) {
database.execSQL(
"ALTER TABLE 'market_data' ADD COLUMN 'watch_list_boolean' TEXT NOT NULL DEFAULT '0'");
}
};
private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
#Override
public void migrate(#NonNull #NotNull SupportSQLiteDatabase database) {
database.execSQL("DROP TABLE 'developer_data'");
}
};
Don't forget to update DB version :)
With all due respect, you are taking a conservative approach.
Since the Room database uses Gradle to set its version number, it's very easy to change it.
So, instead of relying on the tools of Gradle and SQLiteDatabase to do this job for you, use the in-memory version of your database and just create the column using plain SQL.
I am using SQLCipher v3.5.7 and observed an unexpected behavior from SQLiteDatabase with incorrect password.
I encrypted the database with "key1".
Closed the database connection.
Then I tried to open my database with "key2", the SQLiteDatabase is not throwing an exception. Instead, it is updating the old password (key1) to new password (key2). I verified this by opening the .db file in SQLiteBrowser.
Can somebody help me why it is behaving this way?
private static SQLiteCipherDatabaseHelper createDBConnection(Context context, String databasePath, final String key) throws SQLiteDatabaseException {
if (dbInstance == null) {
dbInstance = new SQLiteCipherDatabaseHelper(context, databasePath);
String path = context.getDatabasePath(databasePath).getPath();
File dbPathFile = new File(path);
if (!dbPathFile.exists()) {
dbPathFile.getParentFile().mkdirs();
}
setDatabaseWithDBEncryption(key);
}
return dbInstance;
}
private static void setDatabaseWithDBEncryption(String encryptionKey) throws SQLiteDatabaseException {
loadSQLCipherLibs();
try {
sqliteDatabase = SQLiteDatabase.openOrCreateDatabase(new File(context.getDatabasePath(databasePath).getPath()), encryptionKey, null);
} catch (Exception e) {
SyncLogger.getSharedInstance().logFatal("SQLiteCipherDatabaseHelper", "Failed to open or create database. Please provide a valid encryption key");
throw new SQLiteDatabaseException(SyncErrorCodes.EC_DB_SQLCIPHER_FAILED_TO_OPEN_OR_CREATE_DATABASE, SyncErrorDomains.ED_OFFLINE_OBJECTS, SyncErrorMessages.EM_DB_SQLCIPHER_FAILED_TO_OPEN_OR_CREATE_DATABASE, e);
}
}
Have you upgrade your db version ??
private static final int DATABASE_VERSION = 2;//from 1 to 2
private static class OpenHelper extends SQLiteOpenHelper {
OpenHelper(Context context) // constructor
{
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
#Override
public void onCreate(SQLiteDatabase db) {
}
#Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
//Changes in db mentioned here
}
}
Are you actually populating the database with tables and data after keying it? It seems most likely that for some reason you are recreating the database each time you run the test. Have you verified that the actual database is encrypted by pulling it off the device and examining the file? Perhaps you are recreating a new database each time you run the test, in which case the new key would just be used.
It's worth noting that this behavior is covered in the SQLCipher for Android Test project.
https://github.com/sqlcipher/sqlcipher-android-tests/blob/master/src/main/java/net/zetetic/tests/InvalidPasswordTest.java
If you suspect an issue you can try running the test suite on your device, or create a new test case to verify the behavior with your own code.
Whenever I update my database I get this error. But when I rerun the app as it is, the database gets updated.
android.database.sqlite.SQLiteReadOnlyDatabaseException: attempt to write a readonly database (code 1032)[
Code:
public DBAdapter(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
ctx = context;
db = getWritableDatabase();
}
#Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion != newVersion) {
ctx.deleteDatabase(DATABASE_NAME);
new DBAdapter(ctx);
} else {
super.onUpgrade(db, oldVersion, newVersion);
}
}
As one of the SO answers suggested, I have added this too:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
BTW: I am using SQLiteAssetHelper to create prebuilt database
This is not a solution to prevent this issue but a work around.
public DBAdapter(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
ctx = context;
try {
db = getWritableDatabase();
} catch (SQLiteReadOnlyDatabaseException e){
ctx.startActivity(new Intent(ctx, MainActivity.class));
}
}
#Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion != newVersion) {
ctx.deleteDatabase(DATABASE_NAME);
new DBAdapter(ctx);
} else {
super.onUpgrade(db, oldVersion, newVersion);
}
}
First time when the adapter is initialized, a writable db is created. Then onUpgrade gets called. Here when the database is deleted, the adapter get reinitialized. But the connection of the db is not deleted and persists hence, the second time when db = getWritableDatabase(); is executed SQLiteReadOnlyDatabaseException occurs. The original activity that initialized DBAdapter is restarted. Now the Adapter is reinitialized and the onUpgrade method is not called and hence SQLiteReadOnlyDatabaseException does not occur.
All this process happens very fast and the user experience does not become bad in my case.
Note: new DBAdapter(ctx); does not seem to be necessary and deleteDatabase seems to recreate the adapter. But for caution, I have written this line of code.
I would love to get some information on the cause and solution for this error.
I had some similar issues with Android SQLite databases. I submitted a bug report on it long ago at https://code.google.com/p/android/issues/detail?id=174566. This report discusses my findings on the reasons in more detail. I am not sure if it is related to your issue or not, but it seems to share some characteristics.
To summarize here, my debugging indicated that Android opens the database file, calls onUpgrade(), and if you replace the database file during the onUpgrade() call, the Android side file handle points to the old file and thus causes the app to crash when you return from onUpgrade() and Android tries to access the old file.
Here is some code I used to get around the issue:
When the app starts, I did this in onCreate():
Thread t = new Thread(new Runnable() {
#Override
public void run() {
Context context = getApplicationContext();
DBReader.copyDB(MainActivity.this);
DBReader.initialize(context);
}
});
t.start();
This causes the update of the database file to happen in the background while the app is starting and user is occupied with awe of the awesome application. Because my file was rather big and it took a while to copy. Notice that I completely avoid doing anything in onUpgrade() here.
DBReader is my own class, for which the main code of interest is this:
SharedPreferences prefs = context.getSharedPreferences(Const.KEY_PREFERENCES, Context.MODE_PRIVATE);
//here we have stored the latest version of DB copied
String dbVersion = prefs.getString(Const.KEY_DB_VERSION, "0");
int dbv = Integer.parseInt(dbVersion);
if (checkIfInitialized(context) && dbv == DBHelper.DB_VERSION) {
return;
}
File target = context.getDatabasePath(DBHelper.DB_NAME);
String path = target.getAbsolutePath();
//Log.d("Awesome APP", "Copying database to " + path);
path = path.substring(0, path.lastIndexOf("/"));
File targetDir = new File(path);
targetDir.mkdirs();
//Copy the database from assets
InputStream mInput = context.getAssets().open(DBHelper.DB_NAME);
OutputStream mOutput = new FileOutputStream(target.getAbsolutePath());
byte[] mBuffer = new byte[1024];
int mLength;
while ((mLength = mInput.read(mBuffer)) > 0) {
mOutput.write(mBuffer, 0, mLength);
}
mOutput.flush();
mOutput.close();
mInput.close();
SharedPreferences.Editor edit = prefs.edit();
edit.putString(Const.KEY_DB_VERSION, "" + DBHelper.DB_VERSION);
edit.apply();
and the code for checkIfInitialized():
public static synchronized boolean checkIfInitialized(Context context) {
File dbFile = context.getDatabasePath(DBHelper.DB_NAME);
return dbFile.exists();
}
So, to make the story short, I just avoided onUpgrade() alltogether and implemented my own custom upgrade functionality. This avoids the problem of the Android OS crashing on old and invalid filehandles caused by change of the database in onUpgrade().
Kind of odd, I though, for the onUpgrade() to cause the OS to crash your app if you actually end up upgrading your database file in a function intended to let you upgrade your database. And the Google comments on the bug report were made few years after so I no longer had the original crashing code around for easy proof of concept.
Your problem might be slightly different in that you are not copying the database file, but you still seem to be modifying it, so the root cause might be similar.
I am attempting my first Realm migration in one of my projects using Realm v 0.85.0. I am migrating from v 0.84.0 but the migration should also work for earlier versions. I have followed the example at https://github.com/realm/realm-java/tree/master/examples/migrationExample/src/main that was linked to in the documentation.
In this migration I am attempting to add two new tables. In order to add each new table my migration code looks like the following:
public class Migration implements RealmMigration {
#Override
public long execute(Realm realm, long version) {
if (version == 0)
{
Table newTableOne = realm.getTable(NewTableOne.class);
newTableOne.addColumn(ColumnType.STRING, "columnOne");
// Add any other needed columns here and repeat process for NewTableTwo
// Rest of migration logic goes here...
version++;
}
return version;
}
}
According to Migration on Realm 0.81.1 the getTable() method will automatically create the table if it does not exist. I do not think this is the problem but I've included it just for completeness.
I am also attempting to add a couple of new columns to an existing table and set default values on these new columns. To do this I am using the following code:
Table existingTable = realm.getTable(ExistingTable.class);
existingTable.addColumn(ColumnType.BOOLEAN, "newColumnOne");
existingTable.addColumn(ColumnType.INTEGER, "newColumnTwo");
// Any other new columns needed here
long newColumnOneIndex = getIndexForProperty(existingTable, "newColumnOne");
long newColumnTwoIndex = getIndexForProperty(existingTable, "newColumnTwo");
for (int i = 0; i < existingTable.size(); i++)
{
userTable.setBoolean(newColumnOneIndex, i, false);
userTable.setLong(newColumnTwoIndex, i, 5);
}
The getIndexForProperty method is pulled directly from the example on Github and looks like:
private long getIndexForProperty(Table table, String name) {
for (int i = 0; i < table.getColumnCount(); i++) {
if (table.getColumnName(i).equals(name)) {
return i;
}
}
return -1;
}
When this migration is run I am getting a RealmMigrationNeededException that states "Field count does not match - expected 22 but was 23". I have looked around StackOverflow and done some research via Google and on the Github wiki but have not been able to find any information relating to this exact "Field count does not match" message.
I have ensured that there is an addColumn line for each new field in each model class and there are not more fields in my model than I am adding columns and vice versa.
Any help that you may be able to provide would be greatly appreciated.
From the last 3 days i am trying to upgrade my database to a higher version of SQLCipher library (v3.1.0). I did every step and followed a few tutorials too. But keep on getting the error "File is encrypted or not a Database". Now am trying to move to unencrypted database ie. simple sqlite database.
Do we have a way to move to encrypted database to un-encrypted database? Thanks in advance.
This is the code i am working on:
public MyDBAdapter(Context context) {
this.context = context;
File dbFile = context.getDatabasePath(DATABASE_NAME);
String dbPath = context.getDatabasePath(DATABASE_NAME).toString();
if (dbFile.exists()) {
try {
SQLiteDatabase.loadLibs(context.getApplicationContext());//load SqlCipher libraries
SQLiteDatabase db = getExistDataBaseFile(dbPath, KEY_PASSPHRASE_ENCRYPTION, dbFile);
if (version == 1) {
MigrateDatabaseFrom1xFormatToCurrentFormat(
dbFile, KEY_PASSPHRASE_ENCRYPTION);
}
System.out.println("Old Database found and updated.");
} catch (Exception e) {
System.out.println("No Old Database found");
}
}
this.dbhelper = new MyDBHelper(this.context, DATABASE_NAME, null,
DATABASE_Version);
db = dbhelper.getWritableDatabase(KEY_PASSPHRASE_ENCRYPTION);
}
private SQLiteDatabase getExistDataBaseFile(String FULL_DB_Path, String password, File dbFile) {// this function to open an Exist database
SQLiteDatabase.loadLibs(context.getApplicationContext());
SQLiteDatabaseHook hook = new SQLiteDatabaseHook() {
public void preKey(SQLiteDatabase database) {
System.out.println("-----Inside preKey");
}
public void postKey(SQLiteDatabase database) {
System.out.println("-----Inside postKey");
database.rawExecSQL("PRAGMA cipher_migrate;");
}
};
SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(
dbFile, "Test123", null, hook); // Exception
return database;
}
If you are upgrading your SQLCipher library to the latest version, currently at 3.1.0, and your previous version was 2.x (as you mentioned in the comments above), you will need to upgrade the database file format as well. One of the big changes in the 3.x release was an increase in key derivation length, from 4,000 to 64,000. If you are using all of the standard SQLCipher configurations, upgrading the database format is straight forward. We have included a new PRAGMA call cipher_migrate that will perform this operation for you. You can execute this within the postKey event of the SQLiteDatabaseHook which is to be provided in your call to SQLiteDatabase.openOrCreateDatabase. An example of this can be found in the SQLCipher for Android test suite here.