#pragma warning disable RCS1163 // Unused parameter. /// <summary> /// Alternate common conflict handler, always overwrites changes with new value on conflict. /// Returns no change if conflict is not due to conflict or data issue. /// </summary> /// <param name="ctx">unused</param> /// <param name="eConflict">reason for callback</param> /// <param name="pIter">conflict change set, may iterate through values</param> /// <returns>SQLITE_CHANGESET_REPLACE indicating overwrite with conflicting value</returns> public static ConflictResolution CallbackReplaceOnConflicts(IntPtr pCtx, ConflictReason eConflict, IntPtr pIter) #pragma warning restore RCS1163 // Unused parameter. { #if false // example code only object ctx = null; if (pCtx != IntPtr.Zero) { GCHandle gch = GCHandle.FromIntPtr(pCtx); ctx = gch.Target; } var myCtx = ctx as string; // Note: pIter should be converted to a Sqlite3ChangesetIterator to allow using ChangeSetOp,ChangeSet*Value methods // however must actually create Sqlite3ConflictChangesetIterator to avoid unnecessary finalize of iterator var iter = new Sqlite3ConflictChangesetIterator(pIter); // get table, column, and other information about this change string table; int columnCount; ActionCode op; bool indirect; ChangeSetOp(iter, out table, out columnCount, out op, out indirect); SQLiteValue value; for (int i = 0; i < columnCount; i++) { // on conflict with op=SQLITE_INSERT newvalue is the value to use on REPLACE, conflictvalue is current value (unchanged if omit) // oldvalue is unused with op=SQLITE_INSERT if (ChangeSetNewValue(iter, i, out value) == SQLite3.Result.OK) { object o = value.GetValue(); } if (ChangeSetOldValue(iter, i, out value) == SQLite3.Result.OK) { object o = value.GetValue(); } if (ChangeSetConflictValue(iter, i, out value) == SQLite3.Result.OK) { object o = value.GetValue(); } } #endif // replace only valid on conflict or data if ((eConflict == ConflictReason.SQLITE_CHANGESET_CONFLICT) || (eConflict == ConflictReason.SQLITE_CHANGESET_DATA)) { return(ConflictResolution.SQLITE_CHANGESET_REPLACE); } else { return(ConflictResolution.SQLITE_CHANGESET_OMIT); } }
internal static string FieldsConflictMessage(string responseName, ConflictReason reason) => $"Fields {responseName} conflicts because {ReasonMessage(reason.Message)}. " + "Use different aliases on the fields to fetch both if this was intentional.";
/// <summary> /// Default conflict handler if null is specified, ignores all conflicts and makes no changes /// </summary> /// <param name="ctx">unused</param> /// <param name="eConflict">reason for callback</param> /// <param name="iter">conflict change set, may iterate through values</param> /// <returns>always returns SQLITE_CHANGESET_OMIT</returns> public static ConflictResolution CallbackIgnoreConflicts(IntPtr pCtx, ConflictReason eConflict, IntPtr iter) { return(ConflictResolution.SQLITE_CHANGESET_OMIT); }
/// <summary> /// Conflict handler, allows multiple methods of conflict resolution. /// </summary> /// <param name="ctx">information about how to handler conflicts per table</param> /// <param name="eConflict">reason for callback</param> /// <param name="pIter">conflict change set, may iterate through values</param> /// <returns>how to respond to conflicting value, leave unchanged, replace, or abort</returns> public static ConflictResolution CallbackConflictHandler(IntPtr pCtx, ConflictReason eConflict, IntPtr pIter) { try { // get our context object ctx = null; if (pCtx != IntPtr.Zero) { System.Runtime.InteropServices.GCHandle gch = System.Runtime.InteropServices.GCHandle.FromIntPtr(pCtx); ctx = gch.Target; } var dbSync = ctx as DBSync; var dbConnection = dbSync.db; // Note: pIter should be converted to a Sqlite3ChangesetIterator to allow using ChangeSetOp,ChangeSet*Value methods // however must actually create Sqlite3ConflictChangesetIterator to avoid unnecessary finalize of iterator var iter = new Sqlite3ConflictChangesetIterator(pIter); // get table, column, and other information about this change ChangeSetOp(iter, out string tableName, out int columnCount, out ActionCode op, out bool indirect); // get information about columns in current table var tableInfo = dbSync.tableInformation[tableName]; object currentValue, oldValue, newValue; // get primary key value for current row - NOTE: we support a single or multiple column PK var pkValues = SQLiteSession.ChangeSetPrimaryKeyValues(iter); if (pkValues.Count < 1) { // we require a primary key and failed to get it! return(ConflictResolution.SQLITE_CHANGESET_ABORT); } PrimaryKeyData[] pk = new PrimaryKeyData[pkValues.Count]; for (var i = 0; i < pkValues.Count; i++) { // get primary key column's value pk[i].value = pkValues[i].Item1.GetValue(); // get primary key column's name from index for table pk[i].name = tableInfo[pkValues[i].Item2].Name; } // log some debug information about conflict (data to sync) { var sb = new StringBuilder($"Conflict ({eConflict}) in table '{tableName}' for pk='{pk}', op={op}, Row values:"); sb.AppendLine(); for (int i = 0; i < tableInfo.Count; i++) { GetConflictValues(ref iter, op, i, out currentValue, out oldValue, out newValue); sb.AppendLine($"[{i}] current={currentValue}, old={oldValue}, new={newValue}"); } logger.Debug(sb.ToString()); } // TODO handle constraint violations better if ((eConflict == ConflictReason.SQLITE_CHANGESET_CONSTRAINT) || (eConflict == ConflictReason.SQLITE_CHANGESET_FOREIGN_KEY)) { return(ConflictResolution.SQLITE_CHANGESET_ABORT); } // if op==insert then in current db and not in replica // so is this a new record (keep) or a deleted record in replica (do we want to replicate delete?) // newValue (and currentValue) has current's value including timestamp if available // oldValue is null, need to query replica's shadow table to see if deleted (and timestamp) or if // not found then assume a new row (so keep) // // if op==delete then in replica db and not in current // so is this a new record (insert) or a deleted record in current (do we want keep delete?) // oldValue has the replica's value including timestamp if available // newValue (and currentValue) are null, need to query current's shadow table to see if deleted (and timestamp) or if // not found then assume a new row (so insert) // // if op==update then which do we keep, current or replica => we can do this per column or per row // oldValue and newValue are null then column has no conflict (currentValue has column's value) // oldValue is replica's value, newValue (and currentValue) is current's value // // TODO - how to handle constraint and foreign key violations, structuring order of replicating tables // should avoid these in most cases, for now we punt and abort! // get timestamps from current and replica row, if have a timestamp (-1 for column if not one) int timestampColumn = FindColumn(tableInfo, "timestamp"); DateTime?currentTimestamp = null, replicaTimestamp = null; if (timestampColumn >= 0) { GetConflictValues(ref iter, op, timestampColumn, out currentValue, out oldValue, out newValue); if (op == ActionCode.SQLITE_UPDATE) // both are in conflict values { currentTimestamp = currentValue as DateTime?; replicaTimestamp = (oldValue as DateTime?) ?? currentTimestamp; // no oldValue indicates identical values } else // must query a shadow table { DateTime?shadowTimestamp = null; var query = new StringBuilder($"SELECT [del_timestamp] FROM "); query.Append((op == ActionCode.SQLITE_INSERT) ? "replica" : "main"); query.Append("."); query.Append(tableName); query.Append("_ WHERE "); // shadow table has _ suffix, e.g. [MyTable] shadow table is [MyTable_] var keys = new List <object>(pk.Length); for (var i = 0; i < pk.Length; i++) { if (i != 0) { query.Append(" AND "); } query.Append(pk[i].name); query.Append("=?"); keys.Add(pk[i].value); } query.Append(";"); try { // if no shadow table or not a deleted row then this will throw since doesn't exist shadowTimestamp = dbConnection.ExecuteScalar <DateTime>(query.ToString(), keys.ToArray()); } catch (Exception) { /* swallow error if no shadow record */ } if (op == ActionCode.SQLITE_INSERT) { if (currentValue != null) { currentTimestamp = new DateTime((long)currentValue); } replicaTimestamp = shadowTimestamp; } else { currentTimestamp = shadowTimestamp; if (oldValue != null) { replicaTimestamp = new DateTime((long)oldValue); } } } } // Note: the way we are using Session, we explicitly make the changes // in all cases except on Update where we currently keep or replace whole row // TODO allow selection of individual fields? which would require us to manually // do the update - hence also returning SQLITE_CHANGESET_OMIT var action = ConflictResolution.SQLITE_CHANGESET_OMIT; if (op == ActionCode.SQLITE_INSERT) { // keep current value unless replicaTimestamp is newer if ((currentTimestamp != null) && (replicaTimestamp != null) && (currentTimestamp < replicaTimestamp)) { // TODO - we need to actually issue a DELETE! // TODO - then update shadow table so del_timestamp matches replica } } else if (op == ActionCode.SQLITE_DELETE) { // indicate nothing to do, as we have to manually insert if desired to add action = ConflictResolution.SQLITE_CHANGESET_OMIT; // are we a new row only in replica? or was modified in replica after we deleted so re-add modified value? if ((currentTimestamp == null) || ((replicaTimestamp != null) && (currentTimestamp < replicaTimestamp))) { // then insert it { var sb = new StringBuilder("INSERT INTO "); sb.Append(tableName); sb.Append(" ("); for (var i = 0; i < tableInfo.Count; i++) { if (i != 0) { sb.Append(", "); } sb.Append("["); // double quoted "column names" are standard, but [name] works and easier to read sb.Append(tableInfo[i].Name); // if any chance of user driven names then use " and escape internal "s sb.Append("]"); } sb.Append(") VALUES ("); var values = new object[tableInfo.Count]; for (var i = 0; i < tableInfo.Count; i++) { if (i != 0) { sb.Append(", "); } sb.Append("?"); oldValue = null; if (ChangeSetOldValue(iter, i, out SQLiteNetSessionModule.SQLiteValue value) == SQLite3.Result.OK) { if (value.HasValue()) { oldValue = value.GetValue(); } } values[i] = oldValue; } sb.Append(");"); logger.Debug("Inserting: '{0}' with values={1}", sb.ToString(), statePrinter.PrintObject(values)); dbConnection.Execute(sb.ToString(), values); } } // else assume we deleted on purpose so leave that way } else if (op == ActionCode.SQLITE_UPDATE) { // TODO allow more fine grained selection - for now simply keep latest change if ((currentTimestamp == null) || (replicaTimestamp == null) || (currentTimestamp >= replicaTimestamp)) { action = ConflictResolution.SQLITE_CHANGESET_OMIT; } else { action = ConflictResolution.SQLITE_CHANGESET_REPLACE; } } else { logger.Error($"Unknown or unexpected op {op} - aborting!"); } // replace only valid on conflict or data //if ((eConflict == ConflictReason.SQLITE_CHANGESET_CONFLICT) || (eConflict == ConflictReason.SQLITE_CHANGESET_DATA)) logger.Debug($"returning action={action}\r\n"); return(action); } catch (Exception e) { logger.Error(e, "Unexpected error during conflict handler callback."); return(ConflictResolution.SQLITE_CHANGESET_ABORT); } }