package co.mwater.clientapp.dbsync; import java.util.HashSet; import java.util.Set; import android.content.ContentValues; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.util.Log; import co.mwater.clientapp.dbsync.ChangeSet.Table; import co.mwater.clientapp.dbsync.SyncTable.ForeignKey; public class SyncClientImpl implements SyncClient { private static final String TAG = SyncClientImpl.class.getCanonicalName(); SQLiteDatabase db; SyncTable[] syncTables; public SyncClientImpl(SQLiteDatabase db, SyncTable[] syncTables) { this.db = db; this.syncTables = syncTables; } public ChangeSet getChangeSet() { // Perform in transaction try { db.beginTransaction(); // Get until value long until; Cursor untilCursor = db.rawQuery("SELECT MAX(" + SyncChangesTable.COLUMN_ID + ") FROM " + SyncChangesTable.TABLE_NAME, null); try { if (!untilCursor.moveToFirst() || untilCursor.isNull(0)) return null; until = untilCursor.getLong(0); } finally { untilCursor.close(); } // For each table ChangeSet.Table[] tableChangeSets = new ChangeSet.Table[syncTables.length]; for (int i = 0; i < syncTables.length; i++) { // Get changes for table tableChangeSets[i] = getTableChangeSet(syncTables[i]); } db.setTransactionSuccessful(); return new ChangeSet(until, tableChangeSets); } finally { db.endTransaction(); } } private Table getTableChangeSet(SyncTable syncTable) { Table table = new Table(); table.tableName = syncTable.getTableName(); table.upserts = getUpserts(syncTable, table); table.deletes = getDeletes(syncTable, table); return table; } private Cursor getUpserts(SyncTable syncTable, Table table) { // Get updates // e.g. // SELECT uid, x, y FROM sometable // WHERE uid in (SELECT rowuid FROM syncchanges AS sc1 // WHERE tablename=? AND action='U' // AND NOT EXISTS (SELECT NULL FROM syncchanges AS sc2 WHERE tablename=? // AND sc2.rowuid=sc1.rowuid AND action='D')) // TODO substite row names StringBuffer sql = new StringBuffer(); sql.append("SELECT ").append(SyncTable.COLUMN_UID); // For each column for (String col : syncTable.getSyncColumns()) sql.append(", ").append(col); sql.append(" FROM ").append(syncTable.getTableName()); sql.append(" WHERE ").append(SyncTable.COLUMN_UID).append(" in (SELECT "); sql.append(SyncChangesTable.COLUMN_ROWUID).append(" FROM ").append(SyncChangesTable.TABLE_NAME); sql.append(" AS sc1 WHERE tablename=? AND (action='I' OR action='U'))"); return db.rawQuery(sql.toString(), new String[] { syncTable.getTableName() }); } private Cursor getDeletes(SyncTable syncTable, Table table) { // Get deletes // e.g. // SELECT DISTINCT rowuid FROM syncchanges // WHERE tablename=? AND action='D' // AND NOT EXISTS (SELECT NULL FROM FROM syncchanges AS sc1 // WHERE tablename=? AND sc2.rowuid=sc1.rowuid AND action='I') // TODO substite row names StringBuffer sql = new StringBuffer(); sql.append("SELECT DISTINCT ").append(SyncChangesTable.COLUMN_ROWUID); sql.append(" AS ").append(SyncTable.COLUMN_UID).append(" FROM ").append(SyncChangesTable.TABLE_NAME); sql.append(" AS sc1 WHERE tablename=? AND action='D'"); sql.append(" AND NOT EXISTS (SELECT NULL FROM "); sql.append(SyncChangesTable.TABLE_NAME).append(" AS sc2 WHERE tablename=? AND sc2.rowuid=sc1.rowuid AND action='I')"); return db.rawQuery(sql.toString(), new String[] { syncTable.getTableName(), syncTable.getTableName() }); } public void markChangeSetSent(long until) { // Clear all change rows before and including until db.delete(SyncChangesTable.TABLE_NAME, SyncChangesTable.COLUMN_ID + "<=?", new String[] { Long.toString(until) }); } public void applyChangeSet(ChangeSet changeSet, DataSlice dataSlice) throws PendingChangesException { // Perform in transaction try { db.beginTransaction(); // Check for changes Cursor changes = db.rawQuery("SELECT COUNT(*) FROM " + SyncChangesTable.TABLE_NAME, null); changes.moveToFirst(); if (changes.getLong(0) > 0) throw new PendingChangesException(); changes.close(); // Apply upserts in topological order for (SyncTable syncTable : syncTables) { Table table = changeSet.getTable(syncTable.getTableName()); if (table != null) applyUpserts(syncTable, table.upserts); } // Apply deletes in reverse topological order for (int i = syncTables.length - 1; i >= 0; i--) { SyncTable syncTable = syncTables[i]; Table table = changeSet.getTable(syncTable.getTableName()); if (table != null) applyDeletes(syncTable, table.deletes); } // Mark slice status ContentValues values = new ContentValues(); values.put(DataSlicesTable.COLUMN_SERVERUNTIL, changeSet.getUntil()); if (db.update(DataSlicesTable.TABLE_NAME, values, DataSlicesTable.COLUMN_ID + "=?", new String[] { dataSlice.getSliceId() }) == 0) { // Not found. Insert new row values.put(DataSlicesTable.COLUMN_ID, dataSlice.getSliceId()); db.insert(DataSlicesTable.TABLE_NAME, null, values); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } void applyUpserts(SyncTable syncTable, Cursor upserts) { // Determine columns to ignore Set<String> colsIgnore = new HashSet<String>(); for (String colName : upserts.getColumnNames()) colsIgnore.add(colName); // Remove known columns colsIgnore.remove(SyncTable.COLUMN_UID); colsIgnore.remove(SyncTable.COLUMN_ROW_VERSION); for (String colName : syncTable.getSyncColumns()) colsIgnore.remove(colName); if (upserts.moveToFirst()) { do { ContentValues values = new ContentValues(); DatabaseUtils.cursorRowToContentValues(upserts, values); // Remove columns to ignore for (String colName : colsIgnore) values.remove(colName); // Check presence of foreign keys boolean missingFK = false; for (ForeignKey fk : syncTable.getForeignKeys()) { String fkValue = values.getAsString(fk.column); if (fkValue != null) { Cursor fkCursor = db.query(fk.destTable, null, fk.destColumn + "=?", new String[] { fkValue }, null, null, null); if (fkCursor.getCount()==0) missingFK = true; fkCursor.close(); } } if (missingFK) { Log.e(TAG, "Missing foreign key for "+ syncTable.getTableName()+ ":" + values.getAsString(SyncTable.COLUMN_UID)); continue; } // Attempt update // TODO updates uid to same value if (db.update(syncTable.getTableName(), values, SyncTable.COLUMN_UID + "=?", new String[] { values.getAsString(SyncTable.COLUMN_UID) }) == 0) { // Insert since not present db.insert(syncTable.getTableName(), null, values); } } while (upserts.moveToNext()); } } void applyDeletes(SyncTable syncTable, Cursor deletes) { if (deletes.moveToFirst()) { do { String uid = deletes.getString(deletes.getColumnIndex(SyncTable.COLUMN_UID)); // Set row version to -1 first ContentValues values = new ContentValues(); values.put(SyncTable.COLUMN_ROW_VERSION, -1); db.update(syncTable.getTableName(), values, SyncTable.COLUMN_UID + "=?", new String[] { uid }); db.delete(syncTable.getTableName(), SyncTable.COLUMN_UID + "=?", new String[] { uid }); } while (deletes.moveToNext()); } } public long getUntil(DataSlice dataSlice) { Cursor cursor = db.query(DataSlicesTable.TABLE_NAME, null, DataSlicesTable.COLUMN_ID + "=?", new String[] { dataSlice.getSliceId() }, null, null, null); try { if (!cursor.moveToFirst()) return 0; return cursor.getLong(cursor.getColumnIndexOrThrow(DataSlicesTable.COLUMN_SERVERUNTIL)); } finally { cursor.close(); } } }