package co.mwater.clientapp.dbsync;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
/**
* SyncTable is a database table which is designed to have changes synchronized
* with a central server. It uses a series of triggers to record when changes
* are made to its rows. It also stores a row version with each row so that
* the server is able to correctly merge changes made against older copies
* of rows.
*
* Row version is always zero for newly created local rows. As soon as the
* server receives the row, it begins versioning it at version 1. The client
* does not receive a copy back of the row from the server, but the version
* numbers remain different until the first update.
*
* That is, the row versions on the client refer to the row version against
* which the changes are being made.
*
* Deletes should always cascade, as server might not send deletes of child
* rows.
* @author Clayton
*
*/
public abstract class SyncTable {
public static final String COLUMN_ID = "_id";
public static final String COLUMN_UID = "uid";
public static final String COLUMN_ROW_VERSION = "row_version";
public static final String COLUMN_CREATED_BY = "created_by"; // Optional field with enforced behavior when synced. Must be included in syncColumns
private String[] syncColumns; // Columns, excluding any declared above except created_by
private ForeignKey[] foreignKeys;
public abstract String getTableName();
public abstract String getCreateSql();
public SyncTable(String[] syncColumns, ForeignKey[] foreignKeys) {
this.syncColumns = syncColumns;
this.foreignKeys = foreignKeys;
}
public String[] getSyncColumns() {
return syncColumns;
}
public void onCreate(SQLiteDatabase database) {
database.execSQL(getCreateSql());
// Create trigger to synchronize
database.execSQL(getInsertTriggerSql());
database.execSQL(getUpdateTriggerSql());
database.execSQL(getDeleteTriggerSql());
// Create cascade delete trigger
for (ForeignKey fk : foreignKeys)
database.execSQL(getForeignKeyTriggerSql(fk));
}
String getInsertTriggerSql() {
// Create a trigger which is fired when new rows are inserted with a row version of 0
StringBuilder sql = new StringBuilder();
sql.append("CREATE TRIGGER IF NOT EXISTS ");
sql.append("inserttrigger").append(getTableName());
sql.append(" AFTER INSERT ON ").append(getTableName());
sql.append(" WHEN new.").append(COLUMN_ROW_VERSION).append("=0");
sql.append(" BEGIN INSERT INTO ").append(SyncChangesTable.TABLE_NAME);
sql.append(" (").append(SyncChangesTable.COLUMN_TABLENAME);
sql.append(", ").append(SyncChangesTable.COLUMN_ROWUID);
sql.append(", ").append(SyncChangesTable.COLUMN_ACTION).append(")");
sql.append(" VALUES ('").append(getTableName()).append("',");
sql.append(" new.").append(COLUMN_UID).append(",");
sql.append(" 'I'); END");
return sql.toString();
}
String getUpdateTriggerSql() {
// Create a trigger which is fired when rows are updated without changing the row version
// and without setting row version to -1
StringBuilder sql = new StringBuilder();
sql.append("CREATE TRIGGER IF NOT EXISTS ");
sql.append("updatetrigger").append(getTableName());
sql.append(" AFTER UPDATE ON ").append(getTableName());
sql.append(" WHEN new.").append(COLUMN_ROW_VERSION).append("=old.").append(COLUMN_ROW_VERSION);
sql.append(" AND new.").append(COLUMN_ROW_VERSION).append("<>-1");
sql.append(" BEGIN INSERT INTO ").append(SyncChangesTable.TABLE_NAME);
sql.append(" (").append(SyncChangesTable.COLUMN_TABLENAME);
sql.append(", ").append(SyncChangesTable.COLUMN_ROWUID);
sql.append(", ").append(SyncChangesTable.COLUMN_ACTION).append(")");
sql.append(" VALUES ('").append(getTableName()).append("',");
sql.append(" new.").append(COLUMN_UID).append(",");
sql.append(" 'U'); END");
return sql.toString();
}
String getDeleteTriggerSql() {
// Create a trigger which is fired when rows with a non-negative row version are deleted
// To avoid firing, first update row version to -1
StringBuilder sql = new StringBuilder();
sql.append("CREATE TRIGGER IF NOT EXISTS ");
sql.append("deletetrigger").append(getTableName());
sql.append(" AFTER DELETE ON ").append(getTableName());
sql.append(" WHEN old.").append(COLUMN_ROW_VERSION).append(">=0");
sql.append(" BEGIN INSERT INTO ").append(SyncChangesTable.TABLE_NAME);
sql.append(" (").append(SyncChangesTable.COLUMN_TABLENAME);
sql.append(", ").append(SyncChangesTable.COLUMN_ROWUID);
sql.append(", ").append(SyncChangesTable.COLUMN_ACTION).append(")");
sql.append(" VALUES ('").append(getTableName()).append("',");
sql.append(" old.").append(COLUMN_UID).append(",");
sql.append(" 'D'); END");
return sql.toString();
}
String getForeignKeyTriggerSql(ForeignKey fk) {
// Create a trigger which silently deletes child rows
StringBuilder sql = new StringBuilder();
sql.append("CREATE TRIGGER IF NOT EXISTS ");
sql.append("fktrigger").append(getTableName()).append(fk.column);
sql.append(" AFTER DELETE ON ").append(fk.destTable);
sql.append(" BEGIN UPDATE ").append(getTableName());
sql.append(" SET ").append(SyncTable.COLUMN_ROW_VERSION).append("=-1");
sql.append(" WHERE ").append(fk.column).append("=old.").append(fk.destColumn);
sql.append("; DELETE FROM ").append(getTableName());
sql.append(" WHERE ").append(fk.column).append("=old.").append(fk.destColumn);
sql.append("; END");
return sql.toString();
}
public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) {
Log.w(getTableName(), "Upgrading database from version "
+ oldVersion + " to " + newVersion
+ ", which will destroy all old data");
database.execSQL("DROP TABLE IF EXISTS " + getTableName());
database.execSQL("DROP TRIGGER IF EXISTS inserttrigger" + getTableName());
database.execSQL("DROP TRIGGER IF EXISTS updatetrigger" + getTableName());
database.execSQL("DROP TRIGGER IF EXISTS deletetrigger" + getTableName());
onCreate(database);
}
/**
* Foreign keys. Must be strings
* @author Clayton
*
*/
public static class ForeignKey {
/**
* Creates a foreign key
* @param column column in child table
* @param destTable parent table
* @param destColumn parent column
*/
public ForeignKey(String column, String destTable, String destColumn) {
this.column = column;
this.destTable = destTable;
this.destColumn = destColumn;
}
public final String column;
public final String destTable;
public final String destColumn;
}
public ForeignKey[] getForeignKeys() {
return foreignKeys;
}
}