Exemple #1
0
#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.";
Exemple #3
0
 /// <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);
 }
Exemple #4
0
        /// <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);
            }
        }