From fa3ac5066b17b28538a17ae2b7649ba5fac57d00 Mon Sep 17 00:00:00 2001 From: Romain Bertozzi <romain.bertozzi@savoirfairelinux.com> Date: Tue, 24 May 2016 10:59:18 -0400 Subject: [PATCH] database: set columns name explicitely ORMLite behaviour differs in release mode. Columns and table names are by default optimized out, which can break our queries. This patch sets explicitely names for fields in HistoryCall and HistoryText classes. The names of the fields are not guaranteed to be kept if not set explicitely. Since there are naming changes, a data migration must be performed. An incremental database update engine is now available in DatabaseHelper.java for this purpose. Change-Id: I30ad25347289dc9862306d2c843511f8235167eb Tuleap: #629 --- .../java/cx/ring/history/DatabaseHelper.java | 157 +++++++++++++++--- .../java/cx/ring/history/HistoryCall.java | 34 ++-- .../java/cx/ring/history/HistoryManager.java | 8 +- .../java/cx/ring/history/HistoryText.java | 36 ++-- 4 files changed, 191 insertions(+), 44 deletions(-) diff --git a/ring-android/app/src/main/java/cx/ring/history/DatabaseHelper.java b/ring-android/app/src/main/java/cx/ring/history/DatabaseHelper.java index 0a824e19b..7ca659e2c 100644 --- a/ring-android/app/src/main/java/cx/ring/history/DatabaseHelper.java +++ b/ring-android/app/src/main/java/cx/ring/history/DatabaseHelper.java @@ -18,28 +18,36 @@ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ - package cx.ring.history; import android.content.Context; +import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; import android.util.Log; + import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper; import com.j256.ormlite.dao.Dao; import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.table.TableUtils; import java.sql.SQLException; +import java.util.ArrayList; + +/** + * Database History Version + * 7 : changing columns names. See https://gerrit-ring.savoirfairelinux.com/#/c/4297 + */ /** * Database helper class used to manage the creation and upgrading of your database. This class also usually provides * the DAOs used by the other classes. */ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { - + private static final String TAG = DatabaseHelper.class.getSimpleName(); private static final String DATABASE_NAME = "history.db"; // any time you make changes to your database objects, you may have to increase the database version - private static final int DATABASE_VERSION = 6; + private static final int DATABASE_VERSION = 7; private Dao<HistoryCall, Integer> historyDao = null; private Dao<HistoryText, Integer> historyTextDao = null; @@ -55,12 +63,11 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { @Override public void onCreate(SQLiteDatabase db, ConnectionSource connectionSource) { try { - //TableUtils.dropTable(connectionSource, HistoryCall.class, true); - Log.i(DatabaseHelper.class.getName(), "onCreate"); + Log.d(TAG, "onCreate"); TableUtils.createTable(connectionSource, HistoryCall.class); TableUtils.createTable(connectionSource, HistoryText.class); } catch (SQLException e) { - Log.e(DatabaseHelper.class.getName(), "Can't create database", e); + Log.e(TAG, "Can't create database", e); throw new RuntimeException(e); } } @@ -71,20 +78,13 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { */ @Override public void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource, int oldVersion, int newVersion) { + Log.i(TAG, "onUpgrade " + oldVersion + " -> " + newVersion); try { - Log.i(DatabaseHelper.class.getName(), "onUpgrade " + oldVersion + " -> " + newVersion); - if (oldVersion == 4 && newVersion == 5) { - getTextHistoryDao().executeRaw("ALTER TABLE `historytext` ADD COLUMN read INTEGER;"); - } else { - //TableUtils. - TableUtils.dropTable(connectionSource, HistoryCall.class, true); - TableUtils.dropTable(connectionSource, HistoryText.class, true); - // after we drop the old databases, we create the new ones - onCreate(db, connectionSource); - } - } catch (SQLException e) { - Log.e(DatabaseHelper.class.getName(), "Can't update databases", e); - throw new RuntimeException(e); + updateDatabase(oldVersion, db); + } catch (SQLiteException exc) { + exc.printStackTrace(); + clearDatabase(db); + onCreate(db, connectionSource); } } @@ -98,6 +98,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } return historyDao; } + public Dao<HistoryText, Integer> getTextHistoryDao() throws SQLException { if (historyTextDao == null) { historyTextDao = getDao(HistoryText.class); @@ -114,4 +115,122 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { historyDao = null; historyTextDao = null; } + + /** + * Main method to update the database from an old version to the last + * + * @param fromDatabaseVersion the old version of the database + * @param db the SQLiteDatabase to work with + * @throws SQLiteException + */ + private void updateDatabase(int fromDatabaseVersion, SQLiteDatabase db) throws SQLiteException { + try { + while (fromDatabaseVersion < DATABASE_VERSION) { + switch (fromDatabaseVersion) { + case 6: + updateDatabaseFrom6(db); + break; + } + fromDatabaseVersion++; + } + Log.d(TAG, "Database has been updated to the last version."); + } catch (SQLiteException exc) { + Log.e(TAG, "Database has failed to update to the last version."); + throw exc; + } + } + + /** + * Executes the migration from the database version 6 to the next + * + * @param db the SQLiteDatabase to work with + * @throws SQLiteException + */ + private void updateDatabaseFrom6(SQLiteDatabase db) throws SQLiteException { + if (db != null && db.isOpen()) { + try { + Log.d(TAG, "Will begin migration from database version 6 to next."); + db.beginTransaction(); + //~ Create the new historyCall table and int index + db.execSQL("CREATE TABLE IF NOT EXISTS `historycall` (`accountID` VARCHAR , `callID` VARCHAR , " + + "`call_end` BIGINT , `TIMESTAMP_START` BIGINT , `contactID` BIGINT , " + + "`contactKey` VARCHAR , `direction` INTEGER , `missed` SMALLINT , " + + "`number` VARCHAR , `recordPath` VARCHAR ) ;"); + db.execSQL("CREATE INDEX IF NOT EXISTS `historycall_TIMESTAMP_START_idx` ON `historycall` " + + "( `TIMESTAMP_START` );"); + //~ Create the new historyText table and int indexes + db.execSQL("CREATE TABLE IF NOT EXISTS `historytext` (`accountID` VARCHAR , `callID` VARCHAR , " + + "`contactID` BIGINT , `contactKey` VARCHAR , `direction` INTEGER , " + + "`id` BIGINT , `message` VARCHAR , `number` VARCHAR , `read` SMALLINT , " + + "`TIMESTAMP` BIGINT , PRIMARY KEY (`id`) );"); + db.execSQL("CREATE INDEX IF NOT EXISTS `historytext_TIMESTAMP_idx` ON `historytext` ( `TIMESTAMP` );"); + db.execSQL("CREATE INDEX IF NOT EXISTS `historytext_id_idx` ON `historytext` ( `id` );"); + + Cursor hasATable = db.rawQuery("SELECT name FROM sqlite_master WHERE type=? AND name=?;", + new String[]{"table", "a"}); + if (hasATable.getCount() > 0) { + //~ Copying data from the old table "a" + db.execSQL("INSERT INTO `historycall` (TIMESTAMP_START, call_end, number, missed," + + "direction, recordPath, accountID, contactID, contactKey, callID) " + + "SELECT TIMESTAMP_START,b,c,d,e,f,g,h,i,j FROM a;"); + db.execSQL("DROP TABLE IF EXISTS a_TIMESTAMP_START_idx;"); + db.execSQL("DROP TABLE a;"); + } + hasATable.close(); + + Cursor hasETable = db.rawQuery("SELECT name FROM sqlite_master WHERE type=? AND name=?;", + new String[]{"table", "e"}); + if (hasETable.getCount() > 0) { + //~ Copying data from the old table "e" + db.execSQL("INSERT INTO historytext (id, TIMESTAMP, number, direction, accountID," + + "contactID, contactKey, callID, message, read) " + + "SELECT id,TIMESTAMP,c,d,e,f,g,h,i,j FROM e;"); + //~ Remove old tables "a" and "e" + db.execSQL("DROP TABLE IF EXISTS e_TIMESTAMP_idx;"); + db.execSQL("DROP TABLE IF EXISTS e_id_idx;"); + db.execSQL("DROP TABLE e;"); + } + hasETable.close(); + + db.setTransactionSuccessful(); + db.endTransaction(); + Log.d(TAG, "Migration from database version 6 to next, done."); + } catch (SQLiteException exception) { + Log.e(TAG, "Migration from database version 6 to next, failed."); + throw exception; + } + } + } + + /** + * Removes all the data from the database, ie all the tables. + * + * @param db the SQLiteDatabase to work with + */ + private void clearDatabase(SQLiteDatabase db) { + if (db != null && db.isOpen()) { + Log.d(TAG, "Will clear database."); + ArrayList<String> tableNames = new ArrayList<>(); + Cursor c = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'", null); + if (c.moveToFirst()) { + while (!c.isAfterLast()) { + tableNames.add(c.getString(0)); + c.moveToNext(); + } + } + c.close(); + + try { + db.beginTransaction(); + for (String tableName : tableNames) { + db.execSQL("DROP TABLE " + tableName + ";"); + } + db.setTransactionSuccessful(); + db.endTransaction(); + Log.d(TAG, "Database is cleared"); + } catch (SQLiteException exc) { + exc.printStackTrace(); + } + } + } } diff --git a/ring-android/app/src/main/java/cx/ring/history/HistoryCall.java b/ring-android/app/src/main/java/cx/ring/history/HistoryCall.java index f5f88f14b..c2563fffd 100644 --- a/ring-android/app/src/main/java/cx/ring/history/HistoryCall.java +++ b/ring-android/app/src/main/java/cx/ring/history/HistoryCall.java @@ -27,6 +27,7 @@ import android.os.Parcel; import android.os.Parcelable; import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; import cx.ring.R; import cx.ring.model.SipCall; @@ -37,27 +38,40 @@ import java.util.Date; import java.util.Locale; import java.util.TimeZone; +@DatabaseTable(tableName = HistoryCall.TABLE_NAME) public class HistoryCall implements Parcelable { - @DatabaseField(index = true, columnName = "TIMESTAMP_START") + public static final String TABLE_NAME = "historycall"; + public static final String COLUMN_TIMESTAMP_START_NAME = "TIMESTAMP_START"; + public static final String COLUMN_TIMESTAMP_END_NAME = "call_end"; + public static final String COLUMN_NUMBER_NAME = "number"; + public static final String COLUMN_MISSED_NAME = "missed"; + public static final String COLUMN_DIRECTION_NAME = "direction"; + public static final String COLUMN_RECORD_PATH_NAME = "recordPath"; + public static final String COLUMN_ACCOUNT_ID_NAME = "accountID"; + public static final String COLUMN_CONTACT_ID_NAME = "contactID"; + public static final String COLUMN_CONTACT_KEY_NAME = "contactKey"; + public static final String COLUMN_CALL_ID_NAME = "callID"; + + @DatabaseField(index = true, columnName = COLUMN_TIMESTAMP_START_NAME) public long call_start; - @DatabaseField + @DatabaseField(columnName = COLUMN_TIMESTAMP_END_NAME) public long call_end; - @DatabaseField + @DatabaseField(columnName = COLUMN_NUMBER_NAME) public String number; - @DatabaseField + @DatabaseField(columnName = COLUMN_MISSED_NAME) boolean missed; - @DatabaseField + @DatabaseField(columnName = COLUMN_DIRECTION_NAME) int direction; - @DatabaseField + @DatabaseField(columnName = COLUMN_RECORD_PATH_NAME) String recordPath; - @DatabaseField + @DatabaseField(columnName = COLUMN_ACCOUNT_ID_NAME) String accountID; - @DatabaseField + @DatabaseField(columnName = COLUMN_CONTACT_ID_NAME) long contactID; - @DatabaseField + @DatabaseField(columnName = COLUMN_CONTACT_KEY_NAME) String contactKey; - @DatabaseField + @DatabaseField(columnName = COLUMN_CALL_ID_NAME) String callID; public String getAccountID() { diff --git a/ring-android/app/src/main/java/cx/ring/history/HistoryManager.java b/ring-android/app/src/main/java/cx/ring/history/HistoryManager.java index fafe1dd1e..c0d6ffefb 100644 --- a/ring-android/app/src/main/java/cx/ring/history/HistoryManager.java +++ b/ring-android/app/src/main/java/cx/ring/history/HistoryManager.java @@ -140,13 +140,13 @@ public class HistoryManager { public List<HistoryCall> getAll() throws SQLException { QueryBuilder<HistoryCall, Integer> qb = getHelper().getHistoryDao().queryBuilder(); - qb.orderBy("TIMESTAMP_START", true); + qb.orderBy(HistoryCall.COLUMN_TIMESTAMP_START_NAME, true); return getHelper().getHistoryDao().query(qb.prepare()); } public List<HistoryText> getAllTextMessages() throws SQLException { QueryBuilder<HistoryText, Integer> qb = getHelper().getTextHistoryDao().queryBuilder(); - qb.orderBy("TIMESTAMP", true); + qb.orderBy(HistoryText.COLUMN_TIMESTAMP_NAME, true); return getHelper().getTextHistoryDao().query(qb.prepare()); } @@ -171,7 +171,7 @@ public class HistoryManager { DeleteBuilder<HistoryText, Integer> deleteTextHistoryBuilder = getHelper() .getTextHistoryDao() .deleteBuilder(); - deleteTextHistoryBuilder.where().in("id", textMessagesIds); + deleteTextHistoryBuilder.where().in(HistoryText.COLUMN_ID_NAME, textMessagesIds); deleteTextHistoryBuilder.delete(); //~ Deleting calls ArrayList<String> callIds = new ArrayList<>(entry.getValue().getCalls().size()); @@ -181,7 +181,7 @@ public class HistoryManager { DeleteBuilder<HistoryCall, Integer> deleteCallsHistoryBuilder = getHelper() .getHistoryDao() .deleteBuilder(); - deleteCallsHistoryBuilder.where().in("callId", callIds); + deleteCallsHistoryBuilder.where().in(HistoryCall.COLUMN_CALL_ID_NAME, callIds); deleteCallsHistoryBuilder.delete(); } } catch (SQLException e) { diff --git a/ring-android/app/src/main/java/cx/ring/history/HistoryText.java b/ring-android/app/src/main/java/cx/ring/history/HistoryText.java index 71f5b2b30..1d1bbbe5a 100644 --- a/ring-android/app/src/main/java/cx/ring/history/HistoryText.java +++ b/ring-android/app/src/main/java/cx/ring/history/HistoryText.java @@ -22,33 +22,47 @@ package cx.ring.history; import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; + import java.util.Date; import java.util.Random; import cx.ring.model.TextMessage; +@DatabaseTable(tableName = HistoryText.TABLE_NAME) public class HistoryText { - - @DatabaseField(index = true, columnName="id", id = true) + public static final String TABLE_NAME = "historytext"; + public static final String COLUMN_ID_NAME = "id"; + public static final String COLUMN_TIMESTAMP_NAME = "TIMESTAMP"; + public static final String COLUMN_NUMBER_NAME = "number"; + public static final String COLUMN_DIRECTION_NAME = "direction"; + public static final String COLUMN_ACCOUNT_ID_NAME = "accountID"; + public static final String COLUMN_CONTACT_ID_NAME = "contactID"; + public static final String COLUMN_CONTACT_KEY_NAME = "contactKey"; + public static final String COLUMN_CALL_ID_NAME = "callID"; + public static final String COLUMN_MESSAGE_NAME = "message"; + public static final String COLUMN_READ_NAME = "read"; + + @DatabaseField(index = true, columnName=COLUMN_ID_NAME, id = true) public long id; - @DatabaseField(index = true, columnName="TIMESTAMP") + @DatabaseField(index = true, columnName=COLUMN_TIMESTAMP_NAME) public long time; - @DatabaseField + @DatabaseField(columnName=COLUMN_NUMBER_NAME) public String number; - @DatabaseField + @DatabaseField(columnName=COLUMN_DIRECTION_NAME) public int direction; - @DatabaseField + @DatabaseField(columnName=COLUMN_ACCOUNT_ID_NAME) String accountID; - @DatabaseField + @DatabaseField(columnName=COLUMN_CONTACT_ID_NAME) long contactID; - @DatabaseField + @DatabaseField(columnName=COLUMN_CONTACT_KEY_NAME) String contactKey; - @DatabaseField + @DatabaseField(columnName=COLUMN_CALL_ID_NAME) String callID; - @DatabaseField + @DatabaseField(columnName=COLUMN_MESSAGE_NAME) String message; - @DatabaseField + @DatabaseField(columnName=COLUMN_READ_NAME) boolean read; static private final Random R = new Random(); -- GitLab