/// <summary> /// Apply a single update in the current datasource. if forceWrite, override conflict situation and force the update /// </summary> private async Task <bool> InternalApplyConflictUpdateAsync(SyncContext context, DbSyncAdapter syncAdapter, SyncRow row, long?lastTimestamp, Guid?senderScopeId, bool forceWrite, DbConnection connection, DbTransaction transaction) { if (row.Table == null) { throw new ArgumentException("Schema table is not present in the row"); } var command = await syncAdapter.GetCommandAsync(DbCommandType.UpdateRow, connection, transaction); // Set the parameters value from row syncAdapter.SetColumnParametersValues(command, row); // Set the special parameters for update syncAdapter.AddScopeParametersValues(command, senderScopeId, lastTimestamp, false, forceWrite); var rowUpdatedCount = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { rowUpdatedCount = (int)syncRowCountParam.Value; } return(rowUpdatedCount > 0); }
/// <summary> /// Update a metadata row /// </summary> internal async Task <(SyncContext context, bool metadataUpdated)> InternalUpdateMetadatasAsync(IScopeInfo scopeInfo, SyncContext context, DbSyncAdapter syncAdapter, SyncRow row, Guid?senderScopeId, bool forceWrite, DbConnection connection, DbTransaction transaction) { context.SyncStage = SyncStage.ChangesApplying; var(command, _) = await syncAdapter.GetCommandAsync(DbCommandType.UpdateMetadata, connection, transaction); if (command == null) { return(context, false); } // Set the parameters value from row syncAdapter.SetColumnParametersValues(command, row); // Set the special parameters for update syncAdapter.AddScopeParametersValues(command, senderScopeId, 0, row.RowState == DataRowState.Deleted, forceWrite); await this.InterceptAsync(new DbCommandArgs(context, command, connection, transaction)).ConfigureAwait(false); var metadataUpdatedRowsCount = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { metadataUpdatedRowsCount = (int)syncRowCountParam.Value; } command.Dispose(); return(context, metadataUpdatedRowsCount > 0); }
/// <summary> /// Try to get a source row /// </summary> private async Task <SyncRow> InternalGetConflictRowAsync(SyncContext context, DbSyncAdapter syncAdapter, Guid localScopeId, SyncRow primaryKeyRow, SyncTable schema, DbConnection connection, DbTransaction transaction) { // Get the row in the local repository var command = await syncAdapter.GetCommandAsync(DbCommandType.SelectRow, connection, transaction); // set the primary keys columns as parameters syncAdapter.SetColumnParametersValues(command, primaryKeyRow); // Create a select table based on the schema in parameter + scope columns var changesSet = schema.Schema.Clone(false); var selectTable = DbSyncAdapter.CreateChangesTable(schema, changesSet); using var dataReader = await command.ExecuteReaderAsync().ConfigureAwait(false); if (!dataReader.Read()) { dataReader.Close(); return(null); } // Create a new empty row var syncRow = selectTable.NewRow(); for (var i = 0; i < dataReader.FieldCount; i++) { var columnName = dataReader.GetName(i); // if we have the tombstone value, do not add it to the table if (columnName == "sync_row_is_tombstone") { var isTombstone = Convert.ToInt64(dataReader.GetValue(i)) > 0; syncRow.RowState = isTombstone ? DataRowState.Deleted : DataRowState.Modified; continue; } if (columnName == "update_scope_id") { // var readerScopeId = dataReader.GetValue(i); continue; } var columnValueObject = dataReader.GetValue(i); var columnValue = columnValueObject == DBNull.Value ? null : columnValueObject; syncRow[columnName] = columnValue; } // if syncRow is not a deleted row, we can check for which kind of row it is. if (syncRow != null && syncRow.RowState == DataRowState.Unchanged) { syncRow.RowState = DataRowState.Modified; } dataReader.Close(); return(syncRow); }
/// <summary> /// Update a metadata row /// </summary> internal async Task <bool> InternalUpdateMetadatasAsync(SyncContext context, DbSyncAdapter syncAdapter, SyncRow row, Guid?senderScopeId, bool forceWrite, DbConnection connection, DbTransaction transaction) { var command = await syncAdapter.GetCommandAsync(DbCommandType.UpdateMetadata, connection, transaction); // Set the parameters value from row syncAdapter.SetColumnParametersValues(command, row); // Set the special parameters for update syncAdapter.AddScopeParametersValues(command, senderScopeId, 0, row.RowState == DataRowState.Deleted, forceWrite); var metadataUpdatedRowsCount = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { metadataUpdatedRowsCount = (int)syncRowCountParam.Value; } return(metadataUpdatedRowsCount > 0); }
/// <summary> /// Handle a conflict /// The int returned is the conflict count I need /// </summary> internal async Task <(ChangeApplicationAction, int, DmRow)> HandleConflictAsync(DbSyncAdapter syncAdapter, SyncContext context, SyncConflict conflict, ConflictResolutionPolicy policy, ScopeInfo scope, long fromScopeLocalTimeStamp, DbConnection connection, DbTransaction transaction) { DmRow finalRow = null; var conflictApplyAction = ApplyAction.Continue; (conflictApplyAction, finalRow) = await this.GetConflictActionAsync(context, conflict, policy, connection, transaction); // Default behavior and an error occured if (conflictApplyAction == ApplyAction.Rollback) { conflict.ErrorMessage = "Rollback action taken on conflict"; conflict.Type = ConflictType.ErrorsOccurred; return(ChangeApplicationAction.Rollback, 0, null); } // Local provider wins, update metadata if (conflictApplyAction == ApplyAction.Continue) { var isMergeAction = finalRow != null; var row = isMergeAction ? finalRow : conflict.LocalRow; // Conflict on a line that is not present on the datasource if (row == null) { return(ChangeApplicationAction.Continue, 0, finalRow); } if (row != null) { // if we have a merge action, we apply the row on the server if (isMergeAction) { bool isUpdated = false; bool isInserted = false; // Insert metadata is a merge, actually var commandType = DbCommandType.UpdateMetadata; isUpdated = syncAdapter.ApplyUpdate(row, scope, true); if (!isUpdated) { // Insert the row isInserted = syncAdapter.ApplyInsert(row, scope, true); // Then update the row to mark this row as updated from server // and get it back to client isUpdated = syncAdapter.ApplyUpdate(row, scope, true); commandType = DbCommandType.InsertMetadata; } if (!isUpdated && !isInserted) { throw new Exception("Can't update the merge row."); } // IF we have insert the row in the server side, to resolve the conflict // Whe should update the metadatas correctly if (isUpdated || isInserted) { using (var metadataCommand = syncAdapter.GetCommand(commandType)) { // getting the row updated from server var dmTableRow = syncAdapter.GetRow(row); row = dmTableRow.Rows[0]; // Deriving Parameters syncAdapter.SetCommandParameters(commandType, metadataCommand); // Set the id parameter syncAdapter.SetColumnParametersValues(metadataCommand, row); var version = row.RowState == DmRowState.Deleted ? DmRowVersion.Original : DmRowVersion.Current; Guid?create_scope_id = row["create_scope_id"] != DBNull.Value ? (Guid?)row["create_scope_id"] : null; long createTimestamp = row["create_timestamp", version] != DBNull.Value ? Convert.ToInt64(row["create_timestamp", version]) : 0; // The trick is to force the row to be "created before last sync" // Even if we just inserted it // to be able to get the row in state Updated (and not Added) row["create_scope_id"] = create_scope_id; row["create_timestamp"] = fromScopeLocalTimeStamp - 1; // Update scope id is set to server side Guid?update_scope_id = row["update_scope_id"] != DBNull.Value ? (Guid?)row["update_scope_id"] : null; long updateTimestamp = row["update_timestamp", version] != DBNull.Value ? Convert.ToInt64(row["update_timestamp", version]) : 0; row["update_scope_id"] = null; row["update_timestamp"] = updateTimestamp; // apply local row, set scope.id to null becoz applied locally var rowsApplied = syncAdapter.InsertOrUpdateMetadatas(metadataCommand, row, null); if (!rowsApplied) { throw new Exception("No metadatas rows found, can't update the server side"); } } } } finalRow = isMergeAction ? row : conflict.LocalRow; // We don't do anything on the local provider, so we do not need to return a +1 on syncConflicts count return(ChangeApplicationAction.Continue, 0, finalRow); } return(ChangeApplicationAction.Rollback, 0, finalRow); } // We gonna apply with force the line if (conflictApplyAction == ApplyAction.RetryWithForceWrite) { if (conflict.RemoteRow == null) { // TODO : Should Raise an error ? return(ChangeApplicationAction.Rollback, 0, finalRow); } bool operationComplete = false; // create a localscope to override values var localScope = new ScopeInfo { Name = scope.Name, Timestamp = fromScopeLocalTimeStamp }; DbCommandType commandType = DbCommandType.InsertMetadata; bool needToUpdateMetadata = true; switch (conflict.Type) { // Remote source has row, Local don't have the row, so insert it case ConflictType.RemoteUpdateLocalNoRow: case ConflictType.RemoteInsertLocalNoRow: operationComplete = syncAdapter.ApplyInsert(conflict.RemoteRow, localScope, true); commandType = DbCommandType.InsertMetadata; break; // Conflict, but both have delete the row, so nothing to do case ConflictType.RemoteDeleteLocalDelete: case ConflictType.RemoteDeleteLocalNoRow: operationComplete = true; needToUpdateMetadata = false; break; // The remote has delete the row, and local has insert or update it // So delete the local row case ConflictType.RemoteDeleteLocalUpdate: case ConflictType.RemoteDeleteLocalInsert: operationComplete = syncAdapter.ApplyDelete(conflict.RemoteRow, localScope, true); commandType = DbCommandType.UpdateMetadata; break; // Remote insert and local delete, sor insert again on local // but tracking line exist, so make an update on metadata case ConflictType.RemoteInsertLocalDelete: case ConflictType.RemoteUpdateLocalDelete: operationComplete = syncAdapter.ApplyInsert(conflict.RemoteRow, localScope, true); commandType = DbCommandType.UpdateMetadata; break; // Remote insert and local insert/ update, take the remote row and update the local row case ConflictType.RemoteUpdateLocalInsert: case ConflictType.RemoteUpdateLocalUpdate: case ConflictType.RemoteInsertLocalInsert: case ConflictType.RemoteInsertLocalUpdate: operationComplete = syncAdapter.ApplyUpdate(conflict.RemoteRow, localScope, true); commandType = DbCommandType.UpdateMetadata; break; case ConflictType.RemoteCleanedupDeleteLocalUpdate: case ConflictType.ErrorsOccurred: return(ChangeApplicationAction.Rollback, 0, finalRow); } if (needToUpdateMetadata) { using (var metadataCommand = syncAdapter.GetCommand(commandType)) { // Deriving Parameters syncAdapter.SetCommandParameters(commandType, metadataCommand); // force applying client row, so apply scope.id (client scope here) var rowsApplied = syncAdapter.InsertOrUpdateMetadatas(metadataCommand, conflict.RemoteRow, scope.Id); if (!rowsApplied) { throw new Exception("No metadatas rows found, can't update the server side"); } } } finalRow = conflict.RemoteRow; //After a force update, there is a problem, so raise exception if (!operationComplete) { var ex = $"Can't force operation for applyType {syncAdapter.ApplyType}"; finalRow = null; return(ChangeApplicationAction.Continue, 0, finalRow); } // tableProgress.ChangesApplied += 1; return(ChangeApplicationAction.Continue, 1, finalRow); } return(ChangeApplicationAction.Rollback, 0, finalRow); }