forked from Sebbia/ActiveAndroid
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added automatic migration, tests for automatic migration. It will be …
…applied when database version is lower than specified database version after executing migrations from files.
- Loading branch information
Showing
16 changed files
with
717 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package com.activeandroid.automigration; | ||
|
||
import java.util.Random; | ||
|
||
import android.content.ContentValues; | ||
import android.database.Cursor; | ||
import android.database.sqlite.SQLiteDatabase; | ||
|
||
import com.activeandroid.Cache; | ||
import com.activeandroid.TableInfo; | ||
import com.activeandroid.util.Log; | ||
import com.activeandroid.util.SQLiteUtils; | ||
import com.activeandroid.util.SQLiteUtils.SQLiteType; | ||
|
||
public class AutoMigration { | ||
|
||
public static class IncompatibleColumnTypesException extends RuntimeException { | ||
private static final long serialVersionUID = -6200636421142104030L; | ||
|
||
public IncompatibleColumnTypesException(String table, String column, SQLiteType typeA, SQLiteType typeB) { | ||
super("Failed to match column " + column + " type " + typeA + " to " + typeB + " in " + table + " table"); | ||
} | ||
} | ||
|
||
public static void migrate(SQLiteDatabase db, int newVersion) { | ||
db.beginTransaction(); | ||
try { | ||
for (TableInfo tableInfo : Cache.getTableInfos()) { | ||
processTableInfo(db, tableInfo); | ||
} | ||
db.execSQL("PRAGMA user_version = " + newVersion); | ||
Log.v("Automatic migration successfull, schemas updated to version " + newVersion); | ||
db.setTransactionSuccessful(); | ||
} finally { | ||
db.endTransaction(); | ||
} | ||
} | ||
|
||
private static void processTableInfo(SQLiteDatabase db, TableInfo tableInfo) { | ||
SQLTableInfo sqlTableInfo = getSqlTableInfo(db, tableInfo); | ||
if (sqlTableInfo != null) { | ||
TableDifference tableDifference = new TableDifference(tableInfo, sqlTableInfo); | ||
if (tableDifference.isEmpty() == false) { | ||
applyDifference(db, tableDifference); | ||
} else { | ||
Log.v("Table " + tableInfo.getTableName() + " does not have any difference, skipping it"); | ||
} | ||
} else { | ||
Log.v("Table " + tableInfo.getTableName() + " does not exist. Creating new"); | ||
db.execSQL(SQLiteUtils.createTableDefinition(tableInfo)); | ||
} | ||
} | ||
|
||
private static void applyDifference(SQLiteDatabase db, TableDifference tableDifference) { | ||
TableInfo tableInfo = tableDifference.getTableInfo(); | ||
if (Log.isEnabled()) { | ||
Log.v("Migrating table " + tableInfo.getTableName() + | ||
" from schema '" + tableDifference.getSqlTableInfo().getSchema() + | ||
"' to schema '" + SQLiteUtils.createTableDefinition(tableInfo) + "'"); | ||
} | ||
|
||
if (tableDifference.isOnlyAdd()) { | ||
Log.v("Table " + tableInfo.getTableName() + " has added columns without primary / unique keys, no existing columns affected"); | ||
for (SQLColumnInfo columnInfo : tableDifference.getDifferences().keySet()) { | ||
addColumnToTable(db, tableDifference, columnInfo); | ||
Log.v("Added " + columnInfo.getName() + " column to " + tableInfo.getTableName()); | ||
} | ||
} else { | ||
Log.v("Table " + tableInfo.getTableName() + " has modified existing columns, moving data to newly created table"); | ||
|
||
String temporaryTableName = "TEMP_" + (tableInfo.getTableName() + "_" + new Random().nextInt(1000)); | ||
db.execSQL("ALTER TABLE " + tableInfo.getTableName() + " RENAME TO " + temporaryTableName); | ||
Log.v("Renamed " + tableInfo.getTableName() + " to " + temporaryTableName); | ||
|
||
db.execSQL(SQLiteUtils.createTableDefinition(tableInfo)); | ||
Log.v("Created new table " + tableInfo.getTableName() + " with new schema"); | ||
|
||
transferColumns(db, temporaryTableName, tableDifference); | ||
Log.v("Rows from temporary table " + temporaryTableName + " transferred to newly created table with new schema " + tableInfo.getTableName()); | ||
|
||
db.execSQL("DROP TABLE " + temporaryTableName); | ||
Log.v("Dropped temporary table " + temporaryTableName); | ||
} | ||
} | ||
|
||
private static void addColumnToTable(SQLiteDatabase db, TableDifference tableDifference, SQLColumnInfo columnInfo) { | ||
db.execSQL("ALTER TABLE " + tableDifference.getTableInfo().getTableName() + " ADD COLUMN " + columnInfo.getColumnDefinition()); | ||
} | ||
|
||
private static void transferColumns(SQLiteDatabase db, String sourceTable, TableDifference tableDifference) { | ||
Cursor sourceCursor = db.query(sourceTable, null, null, null, null, null, null); | ||
ContentValues contentValues = new ContentValues(); | ||
try { | ||
|
||
while (sourceCursor.moveToNext()) { | ||
contentValues.clear(); | ||
for (SQLColumnInfo columnInfo : tableDifference.getNewSchemaColumnInfos()) { | ||
if (tableDifference.getDifferences().containsKey(columnInfo)) { | ||
SQLColumnInfo mappedColumnInfo = tableDifference.getDifferences().get(columnInfo); | ||
if (mappedColumnInfo != null) { | ||
putValueFromCursor(contentValues, sourceCursor, mappedColumnInfo, columnInfo); | ||
} | ||
} else { | ||
putValueFromCursor(contentValues, sourceCursor, columnInfo, columnInfo); | ||
} | ||
} | ||
db.insert(tableDifference.getTableInfo().getTableName(), null, contentValues); | ||
} | ||
} finally { | ||
sourceCursor.close(); | ||
} | ||
} | ||
|
||
private static void putValueFromCursor(ContentValues contentValues, Cursor cursor, SQLColumnInfo sourceColumnInfo, SQLColumnInfo targetColumnInfo) { | ||
switch (sourceColumnInfo.getType()) { | ||
case INTEGER: | ||
contentValues.put(targetColumnInfo.getName(), cursor.getInt(cursor.getColumnIndex(sourceColumnInfo.getName()))); | ||
break; | ||
|
||
case TEXT: | ||
contentValues.put(targetColumnInfo.getName(), cursor.getString(cursor.getColumnIndex(sourceColumnInfo.getName()))); | ||
break; | ||
|
||
case REAL: | ||
contentValues.put(targetColumnInfo.getName(), cursor.getDouble(cursor.getColumnIndex(sourceColumnInfo.getName()))); | ||
break; | ||
|
||
case BLOB: | ||
contentValues.put(targetColumnInfo.getName(), cursor.getBlob(cursor.getColumnIndex(sourceColumnInfo.getName()))); | ||
break; | ||
} | ||
} | ||
|
||
private static SQLTableInfo getSqlTableInfo(SQLiteDatabase db, TableInfo tableInfo) { | ||
Cursor cursor = db.query("sqlite_master", new String[] { "sql" }, "tbl_name = ?", new String[] { tableInfo.getTableName() }, null, null, null); | ||
if (cursor.moveToNext()) { | ||
return new SQLTableInfo(cursor.getString(0)); | ||
} | ||
return null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package com.activeandroid.automigration; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Locale; | ||
|
||
import com.activeandroid.util.SQLiteUtils.SQLiteType; | ||
|
||
import android.text.TextUtils; | ||
|
||
public class SQLColumnInfo { | ||
|
||
private String mColumnDefinition; | ||
private String mName; | ||
private SQLiteType mType; | ||
|
||
public SQLColumnInfo(String columnDefinition) { | ||
ArrayList<String> tokens = new ArrayList<String>(); | ||
for (String token : columnDefinition.split(" ")) { | ||
if (TextUtils.isEmpty(token) == false) | ||
tokens.add(token); | ||
} | ||
|
||
if (tokens.size() < 2) | ||
throw new IllegalArgumentException("Failed to parse '" + columnDefinition + "' as sql column definition"); | ||
|
||
this.mColumnDefinition = TextUtils.join(" ", tokens.subList(1, tokens.size())); | ||
|
||
this.mName = tokens.get(0); | ||
this.mType = SQLiteType.valueOf(tokens.get(1).toUpperCase(Locale.US)); | ||
|
||
} | ||
|
||
public String getName() { | ||
return mName; | ||
} | ||
|
||
public SQLiteType getType() { | ||
return mType; | ||
} | ||
|
||
public String getColumnDefinition() { | ||
return mName + " " + mColumnDefinition; | ||
} | ||
|
||
public boolean isPrimaryKey() { | ||
return mColumnDefinition.toUpperCase(Locale.US).contains("PRIMARY KEY"); | ||
} | ||
|
||
public boolean isUnique() { | ||
return mColumnDefinition.toUpperCase(Locale.US).contains("UNIQUE"); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package com.activeandroid.automigration; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Locale; | ||
|
||
import android.text.TextUtils; | ||
|
||
public final class SQLTableInfo { | ||
|
||
public static String constructSchema(String tableName, List<SQLColumnInfo> columns) { | ||
String schema = "CREATE TABLE " + tableName + "(%s);"; | ||
List<String> tokens = new ArrayList<String>(); | ||
for (SQLColumnInfo column : columns) { | ||
tokens.add(column.getColumnDefinition()); | ||
} | ||
return String.format(schema, TextUtils.join(", ", tokens)); | ||
} | ||
|
||
//Note that this class does not validate SQL syntax | ||
|
||
private String mTableName; | ||
private SQLColumnInfo mIdColumnInfo; | ||
private List<SQLColumnInfo> mColumns; | ||
private String mSchema; | ||
|
||
public SQLTableInfo(String sqlSchema) { | ||
|
||
if (TextUtils.isEmpty(sqlSchema)) | ||
throw new IllegalArgumentException("Cannot construct SqlTableInfo from empty sqlSchema"); | ||
|
||
sqlSchema = sqlSchema.replaceAll("\\s+", " "); | ||
this.mSchema = new String(sqlSchema); | ||
|
||
if (!sqlSchema.toUpperCase(Locale.US).startsWith("CREATE TABLE") || !sqlSchema.contains("(") || !sqlSchema.contains(")")) | ||
throw new IllegalArgumentException("sqlSchema doesn't appears to be valid"); | ||
mColumns = new ArrayList<SQLColumnInfo>(); | ||
|
||
sqlSchema = sqlSchema.replaceAll("(?i)CREATE TABLE ", ""); | ||
mTableName = sqlSchema.substring(0, sqlSchema.indexOf('(')).replace("\"", ""); | ||
|
||
String columnDefinitions = sqlSchema.substring(sqlSchema.indexOf('(') + 1, sqlSchema.lastIndexOf(')')); | ||
processColumnsDefinitions(columnDefinitions.split(",")); | ||
} | ||
|
||
private void processColumnsDefinitions(String[] columns) { | ||
for (String columnDef : columns) { | ||
SQLColumnInfo columnInfo = new SQLColumnInfo(columnDef); | ||
if (columnInfo.isPrimaryKey()) { | ||
if (mIdColumnInfo == null) | ||
mIdColumnInfo = columnInfo; | ||
else | ||
throw new IllegalArgumentException("sqlSchema contains multiple primary keys"); | ||
} | ||
|
||
mColumns.add(columnInfo); | ||
} | ||
} | ||
|
||
public String getSchema() { | ||
return mSchema; | ||
} | ||
|
||
public SQLColumnInfo getIdColumnInfo() { | ||
return mIdColumnInfo; | ||
} | ||
|
||
public List<SQLColumnInfo> getColumns() { | ||
return mColumns; | ||
} | ||
|
||
public String getTableName() { | ||
return mTableName; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package com.activeandroid.automigration; | ||
|
||
import java.lang.reflect.Field; | ||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import com.activeandroid.TableInfo; | ||
import com.activeandroid.automigration.AutoMigration.IncompatibleColumnTypesException; | ||
import com.activeandroid.util.SQLiteUtils; | ||
|
||
class TableDifference { | ||
|
||
|
||
private TableInfo mTableInfo; | ||
private SQLTableInfo mSqlTableInfo; | ||
private Map<SQLColumnInfo, SQLColumnInfo> mDifferences; | ||
private List<SQLColumnInfo> mCurrentVersionTableDefinitions; | ||
|
||
public TableDifference(TableInfo tableInfo, SQLTableInfo sqlTableInfo) { | ||
this.mTableInfo = tableInfo; | ||
this.mSqlTableInfo = sqlTableInfo; | ||
this.mDifferences = new HashMap<SQLColumnInfo, SQLColumnInfo>(); | ||
this.mCurrentVersionTableDefinitions = new ArrayList<SQLColumnInfo>(); | ||
|
||
for (Field field : tableInfo.getFields()) { | ||
SQLColumnInfo sqlColumnInfo = new SQLColumnInfo(SQLiteUtils.createColumnDefinition(tableInfo, field)); | ||
mCurrentVersionTableDefinitions.add(sqlColumnInfo); | ||
|
||
boolean found = false; | ||
for (SQLColumnInfo existingColumnInfo : sqlTableInfo.getColumns()) { | ||
if (existingColumnInfo.getName().equalsIgnoreCase(sqlColumnInfo.getName()) == false) | ||
continue; | ||
|
||
found = true; | ||
|
||
if (existingColumnInfo.getColumnDefinition().equalsIgnoreCase(sqlColumnInfo.getColumnDefinition()) == false) { | ||
if (existingColumnInfo.getType() == sqlColumnInfo.getType()) { | ||
mDifferences.put(sqlColumnInfo, existingColumnInfo); | ||
} else { | ||
throw new IncompatibleColumnTypesException(tableInfo.getTableName(), existingColumnInfo.getName(), existingColumnInfo.getType(), sqlColumnInfo.getType()); | ||
} | ||
} | ||
break; | ||
} | ||
if (!found) | ||
mDifferences.put(sqlColumnInfo, null); | ||
} | ||
} | ||
|
||
public boolean isOnlyAdd() { | ||
for (SQLColumnInfo sqlColumnInfo : mDifferences.keySet()) { | ||
if (mDifferences.get(sqlColumnInfo) != null || sqlColumnInfo.isPrimaryKey() || sqlColumnInfo.isUnique()) | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
public boolean isEmpty() { | ||
return mDifferences.size() == 0; | ||
} | ||
|
||
public Map<SQLColumnInfo, SQLColumnInfo> getDifferences() { | ||
return mDifferences; | ||
} | ||
|
||
public List<SQLColumnInfo> getNewSchemaColumnInfos() { | ||
return mCurrentVersionTableDefinitions; | ||
} | ||
|
||
public TableInfo getTableInfo() { | ||
return mTableInfo; | ||
} | ||
|
||
public SQLTableInfo getSqlTableInfo() { | ||
return mSqlTableInfo; | ||
} | ||
} |
Binary file not shown.
Oops, something went wrong.