private DbCommand InternalSetDeleteClientScopeInfoParameters(ClientScopeInfo clientScopeInfo, DbCommand command) { DbSyncAdapter.SetParameterValue(command, "sync_scope_id", clientScopeInfo.Id.ToString()); DbSyncAdapter.SetParameterValue(command, "sync_scope_name", clientScopeInfo.Name); return(command); }
InternalExistsServerHistoryScopeInfoAsync(string scopeId, string scopeName, SyncContext context, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { // Get exists command var scopeBuilder = this.GetScopeBuilder(this.Options.ScopeInfoTableName); using var existsCommand = scopeBuilder.GetCommandAsync(DbScopeCommandType.ExistServerHistoryScopeInfo, connection, transaction); if (existsCommand == null) { return(context, false); } DbSyncAdapter.SetParameterValue(existsCommand, "sync_scope_name", scopeName); DbSyncAdapter.SetParameterValue(existsCommand, "sync_scope_Id", scopeId); if (existsCommand == null) { return(context, false); } await this.InterceptAsync(new DbCommandArgs(context, existsCommand, connection, transaction), progress, cancellationToken).ConfigureAwait(false); var existsResultObject = await existsCommand.ExecuteScalarAsync().ConfigureAwait(false); var exists = Convert.ToInt32(existsResultObject) > 0; return(context, exists); }
/// <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> /// Set common parameters to SelectChanges Sql command /// </summary> internal void SetSelectChangesCommonParameters(SyncContext context, SyncTable syncTable, Guid?excludingScopeId, bool isNew, long?lastTimestamp, DbCommand selectIncrementalChangesCommand) { // Set the parameters DbSyncAdapter.SetParameterValue(selectIncrementalChangesCommand, "sync_min_timestamp", lastTimestamp); DbSyncAdapter.SetParameterValue(selectIncrementalChangesCommand, "sync_scope_id", excludingScopeId.HasValue ? (object)excludingScopeId.Value : DBNull.Value); // Check filters SyncFilter tableFilter = null; // Sqlite does not have any filter, since he can't be server side if (this.Provider.CanBeServerProvider) { tableFilter = syncTable.GetFilter(); } var hasFilters = tableFilter != null; if (!hasFilters) { return; } // context parameters can be null at some point. var contexParameters = context.Parameters ?? new SyncParameters(); foreach (var filterParam in tableFilter.Parameters) { var parameter = contexParameters.FirstOrDefault(p => p.Name.Equals(filterParam.Name, SyncGlobalization.DataSourceStringComparison)); object val = parameter?.Value; DbSyncAdapter.SetParameterValue(selectIncrementalChangesCommand, filterParam.Name, val); } }
/// <summary> /// Set command parameters value mapped to Row /// </summary> internal void SetColumnParametersValues(DbCommand command, SyncRow row) { if (row.SchemaTable == null) { throw new ArgumentException("Schema table columns does not correspond to row values"); } var schemaTable = row.SchemaTable; foreach (DbParameter parameter in command.Parameters) { if (!string.IsNullOrEmpty(parameter.SourceColumn)) { // foreach parameter, check if we have a column var column = schemaTable.Columns[parameter.SourceColumn]; if (column != null) { object value = row[column] ?? DBNull.Value; DbSyncAdapter.SetParameterValue(command, parameter.ParameterName, value); } } } // return value var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { syncRowCountParam.Direction = ParameterDirection.Output; syncRowCountParam.Value = DBNull.Value; } }
/// <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> /// Reset a table, deleting rows from table and tracking_table /// </summary> internal async Task <bool> InternalResetTableAsync(SyncContext context, DbSyncAdapter syncAdapter, DbConnection connection, DbTransaction transaction) { var command = await syncAdapter.GetCommandAsync(DbCommandType.Reset, connection, transaction); var rowCount = await command.ExecuteNonQueryAsync().ConfigureAwait(false); return(rowCount > 0); }
/// <summary> /// Add common parameters which could be part of the command /// if not found, no set done /// </summary> internal void AddScopeParametersValues(DbCommand command, Guid?id, long?lastTimestamp, bool isDeleted, bool forceWrite) { // Dotmim.Sync parameters DbSyncAdapter.SetParameterValue(command, "sync_force_write", forceWrite ? 1 : 0); DbSyncAdapter.SetParameterValue(command, "sync_min_timestamp", lastTimestamp.HasValue ? (object)lastTimestamp.Value : DBNull.Value); DbSyncAdapter.SetParameterValue(command, "sync_scope_id", id.HasValue ? (object)id.Value : DBNull.Value); DbSyncAdapter.SetParameterValue(command, "sync_row_is_tombstone", isDeleted); }
/// <summary> /// Gets a batch of changes to synchronize when given batch size, /// destination knowledge, and change data retriever parameters. /// </summary> /// <returns>A DbSyncContext object that will be used to retrieve the modified data.</returns> public virtual (SyncContext, BatchInfo) GetSnapshot( SyncContext context, SyncSet schema, string batchDirectory, CancellationToken cancellationToken, IProgress <ProgressArgs> progress = null) { var sb = new StringBuilder(); var underscore = ""; if (context.Parameters != null) { foreach (var p in context.Parameters.OrderBy(p => p.Name)) { var cleanValue = new string(p.Value.ToString().Where(char.IsLetterOrDigit).ToArray()); var cleanName = new string(p.Name.Where(char.IsLetterOrDigit).ToArray()); sb.Append($"{underscore}{cleanName}_{cleanValue}"); underscore = "_"; } } var directoryName = sb.ToString(); directoryName = string.IsNullOrEmpty(directoryName) ? "ALL" : directoryName; var directoryFullPath = Path.Combine(batchDirectory, directoryName); // if no snapshot present, just return null value. if (!Directory.Exists(directoryFullPath)) { return(context, null); } // Serialize on disk. var jsonConverter = new JsonConverter <BatchInfo>(); var summaryFileName = Path.Combine(directoryFullPath, "summary.json"); BatchInfo batchInfo = null; // Create the schema changeset var changesSet = new SyncSet(schema.ScopeName); // Create a Schema set without readonly columns, attached to memory changes foreach (var table in schema.Tables) { DbSyncAdapter.CreateChangesTable(schema.Tables[table.TableName, table.SchemaName], changesSet); } using (var fs = new FileStream(summaryFileName, FileMode.Open, FileAccess.Read)) { batchInfo = jsonConverter.Deserialize(fs); } batchInfo.SetSchema(changesSet); return(context, batchInfo); }
/// <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); }
InternalLoadClientScopeInfoAsync(SyncContext context, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { var scopeBuilder = this.GetScopeBuilder(this.Options.ScopeInfoTableName); using var command = scopeBuilder.GetCommandAsync(DbScopeCommandType.GetClientScopeInfo, connection, transaction); if (command == null) { return(context, null); } DbSyncAdapter.SetParameterValue(command, "sync_scope_name", context.ScopeName); var action = new ClientScopeInfoLoadingArgs(context, context.ScopeName, command, connection, transaction); await this.InterceptAsync(action, progress, cancellationToken).ConfigureAwait(false); if (action.Cancel || action.Command == null) { return(context, null); } await this.InterceptAsync(new DbCommandArgs(context, action.Command, connection, transaction), progress, cancellationToken).ConfigureAwait(false); using DbDataReader reader = await action.Command.ExecuteReaderAsync().ConfigureAwait(false); ClientScopeInfo clientScopeInfo = null; if (reader.Read()) { clientScopeInfo = InternalReadClientScopeInfo(reader); } reader.Close(); if (clientScopeInfo?.Schema != null) { clientScopeInfo.Schema.EnsureSchema(); } if (clientScopeInfo != null) { var scopeLoadedArgs = new ClientScopeInfoLoadedArgs(context, context.ScopeName, clientScopeInfo, connection, transaction); await this.InterceptAsync(scopeLoadedArgs, progress, cancellationToken).ConfigureAwait(false); clientScopeInfo = scopeLoadedArgs.ClientScopeInfo; } action.Command.Dispose(); return(context, clientScopeInfo); }
private DbCommand InternalSetSaveServerScopeInfoParameters(ServerScopeInfo serverScopeInfo, DbCommand command) { var serializedSchema = JsonConvert.SerializeObject(serverScopeInfo.Schema); var serializedSetup = JsonConvert.SerializeObject(serverScopeInfo.Setup); DbSyncAdapter.SetParameterValue(command, "sync_scope_name", serverScopeInfo.Name); DbSyncAdapter.SetParameterValue(command, "sync_scope_schema", serverScopeInfo.Schema == null ? DBNull.Value : serializedSchema); DbSyncAdapter.SetParameterValue(command, "sync_scope_setup", serverScopeInfo.Setup == null ? DBNull.Value : serializedSetup); DbSyncAdapter.SetParameterValue(command, "sync_scope_version", serverScopeInfo.Version); DbSyncAdapter.SetParameterValue(command, "sync_scope_last_clean_timestamp", serverScopeInfo.LastCleanupTimestamp); return(command); }
private DbCommand InternalSetSaveClientScopeInfoParameters(ClientScopeInfo clientScopeInfo, DbCommand command) { DbSyncAdapter.SetParameterValue(command, "sync_scope_id", clientScopeInfo.Id.ToString()); DbSyncAdapter.SetParameterValue(command, "sync_scope_name", clientScopeInfo.Name); DbSyncAdapter.SetParameterValue(command, "sync_scope_schema", clientScopeInfo.Schema == null ? DBNull.Value : (object)JsonConvert.SerializeObject(clientScopeInfo.Schema)); DbSyncAdapter.SetParameterValue(command, "sync_scope_setup", clientScopeInfo.Setup == null ? DBNull.Value : (object)JsonConvert.SerializeObject(clientScopeInfo.Setup)); DbSyncAdapter.SetParameterValue(command, "sync_scope_version", clientScopeInfo.Version); DbSyncAdapter.SetParameterValue(command, "scope_last_sync", clientScopeInfo.LastSync.HasValue ? (object)clientScopeInfo.LastSync.Value : DBNull.Value); DbSyncAdapter.SetParameterValue(command, "scope_last_sync_timestamp", clientScopeInfo.LastSyncTimestamp); DbSyncAdapter.SetParameterValue(command, "scope_last_server_sync_timestamp", clientScopeInfo.LastServerSyncTimestamp); DbSyncAdapter.SetParameterValue(command, "scope_last_sync_duration", clientScopeInfo.LastSyncDuration); return(command); }
/// <summary> /// Get the correct Select changes command /// Can be either /// - SelectInitializedChanges : All changes for first sync /// - SelectChanges : All changes filtered by timestamp /// - SelectInitializedChangesWithFilters : All changes for first sync with filters /// - SelectChangesWithFilters : All changes filtered by timestamp with filters /// </summary> private DbCommand GetSelectChangesCommand(SyncContext context, DbSyncAdapter syncAdapter, SyncTable syncTable, bool isNew) { DbCommand selectIncrementalChangesCommand; DbCommandType dbCommandType; SyncFilter tableFilter = null; // Check if we have parameters specified // Sqlite does not have any filter, since he can't be server side if (this.CanBeServerProvider) { tableFilter = syncTable.GetFilter(); } var hasFilters = tableFilter != null; // Determing the correct DbCommandType if (isNew && hasFilters) { dbCommandType = DbCommandType.SelectInitializedChangesWithFilters; } else if (isNew && !hasFilters) { dbCommandType = DbCommandType.SelectInitializedChanges; } else if (!isNew && hasFilters) { dbCommandType = DbCommandType.SelectChangesWithFilters; } else { dbCommandType = DbCommandType.SelectChanges; } // Get correct Select incremental changes command selectIncrementalChangesCommand = syncAdapter.GetCommand(dbCommandType, tableFilter); if (selectIncrementalChangesCommand == null) { throw new MissingCommandException(dbCommandType.ToString()); } // Add common parameters syncAdapter.SetCommandParameters(dbCommandType, selectIncrementalChangesCommand, tableFilter); return(selectIncrementalChangesCommand); }
internal virtual async Task <DatabaseMetadatasCleaned> InternalDeleteMetadatasAsync(SyncContext context, SyncSet schema, SyncSetup setup, long timestampLimit, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { await this.InterceptAsync(new MetadataCleaningArgs(context, this.Setup, timestampLimit, connection, transaction), cancellationToken).ConfigureAwait(false); DatabaseMetadatasCleaned databaseMetadatasCleaned = new DatabaseMetadatasCleaned { TimestampLimit = timestampLimit }; foreach (var syncTable in schema.Tables) { // Create sync adapter var syncAdapter = this.GetSyncAdapter(syncTable, setup); var command = await syncAdapter.GetCommandAsync(DbCommandType.DeleteMetadata, connection, transaction); // Set the special parameters for delete metadata DbSyncAdapter.SetParameterValue(command, "sync_row_timestamp", timestampLimit); var rowsCleanedCount = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { rowsCleanedCount = (int)syncRowCountParam.Value; } // Only add a new table metadata stats object, if we have, at least, purged 1 or more rows if (rowsCleanedCount > 0) { var tableMetadatasCleaned = new TableMetadatasCleaned(syncTable.TableName, syncTable.SchemaName) { RowsCleanedCount = rowsCleanedCount, TimestampLimit = timestampLimit }; databaseMetadatasCleaned.Tables.Add(tableMetadatasCleaned); } } await this.InterceptAsync(new MetadataCleanedArgs(context, databaseMetadatasCleaned, connection), cancellationToken).ConfigureAwait(false); return(databaseMetadatasCleaned); }
/// <summary> /// Internal update untracked rows routine /// </summary> internal async Task <int> InternalUpdateUntrackedRowsAsync(SyncContext ctx, DbSyncAdapter syncAdapter, DbConnection connection, DbTransaction transaction) { // Get correct Select incremental changes command var command = await syncAdapter.GetCommandAsync(DbCommandType.UpdateUntrackedRows, connection, transaction); // Execute var rowAffected = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { rowAffected = (int)syncRowCountParam.Value; } return(rowAffected); }
/// <summary> /// Internal load all scopes routine /// </summary> internal async Task <List <T> > InternalGetAllScopesAsync <T>(SyncContext ctx, DbScopeType scopeType, string scopeName, DbScopeBuilder scopeBuilder, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) where T : class { var command = scopeBuilder.GetCommandAsync(DbScopeCommandType.GetScopes, scopeType, connection, transaction); if (command == null) { return(null); } DbSyncAdapter.SetParameterValue(command, "sync_scope_name", scopeName); var action = new ScopeLoadingArgs(ctx, scopeName, scopeType, command, connection, transaction); await this.InterceptAsync(action, cancellationToken).ConfigureAwait(false); if (action.Cancel || action.Command == null) { return(null); } var scopes = new List <T>(); using DbDataReader reader = await action.Command.ExecuteReaderAsync().ConfigureAwait(false); while (reader.Read()) { T scopeInfo = scopeType switch { DbScopeType.Server => ReaderServerScopeInfo(reader) as T, DbScopeType.ServerHistory => ReadServerHistoryScopeInfo(reader) as T, DbScopeType.Client => ReadScopeInfo(reader) as T, _ => throw new NotImplementedException($"Can't get {scopeType} from the reader ") }; if (scopeInfo != null) { scopes.Add(scopeInfo); } } reader.Close(); return(scopes); }
/// <summary> /// Internal exists scope /// </summary> internal async Task <bool> InternalExistsScopeInfoAsync(SyncContext ctx, DbScopeType scopeType, string scopeId, DbScopeBuilder scopeBuilder, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { // Get exists command var existsCommand = scopeBuilder.GetCommandAsync(DbScopeCommandType.ExistScope, scopeType, connection, transaction); // Just in case, in older version we may have sync_scope_name as primary key; DbSyncAdapter.SetParameterValue(existsCommand, "sync_scope_name", scopeId); // Set primary key value DbSyncAdapter.SetParameterValue(existsCommand, "sync_scope_id", scopeId); if (existsCommand == null) { return(false); } var existsResultObject = await existsCommand.ExecuteScalarAsync().ConfigureAwait(false); var exists = Convert.ToInt32(existsResultObject) > 0; return(exists); }
/// <summary> /// Internal update untracked rows routine /// </summary> internal async Task <(SyncContext context, int updated)> InternalUpdateUntrackedRowsAsync(IScopeInfo scopeInfo, SyncContext context, DbSyncAdapter syncAdapter, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken = default, IProgress <ProgressArgs> progress = null) { // Get table builder var tableBuilder = this.GetTableBuilder(syncAdapter.TableDescription, scopeInfo); // Check if tracking table exists bool trackingTableExists; (context, trackingTableExists) = await this.InternalExistsTrackingTableAsync(scopeInfo, context, tableBuilder, connection, transaction, CancellationToken.None, null).ConfigureAwait(false); if (!trackingTableExists) { throw new MissingTrackingTableException(tableBuilder.TableDescription.GetFullName()); } // Get correct Select incremental changes command var(command, _) = await syncAdapter.GetCommandAsync(DbCommandType.UpdateUntrackedRows, connection, transaction); if (command == null) { return(context, 0); } await this.InterceptAsync(new DbCommandArgs(context, command, connection, transaction), progress, cancellationToken).ConfigureAwait(false); // Execute var rowAffected = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { rowAffected = (int)syncRowCountParam.Value; } command.Dispose(); return(context, rowAffected); }
/// <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> /// Generate an empty BatchInfo /// </summary> internal (BatchInfo, DatabaseChangesSelected) GetEmptyChanges(MessageGetChangesBatch message) { // Get config var isBatched = message.BatchSize > 0; // create the in memory changes set var changesSet = new SyncSet(message.Schema.ScopeName); // Create a Schema set without readonly tables, attached to memory changes foreach (var table in message.Schema.Tables) { DbSyncAdapter.CreateChangesTable(message.Schema.Tables[table.TableName, table.SchemaName], changesSet); } // Create the batch info, in memory var batchInfo = new BatchInfo(!isBatched, changesSet, message.BatchDirectory);; // add changes to batchInfo batchInfo.AddChanges(new SyncSet()); // Create a new empty in-memory batch info return(batchInfo, new DatabaseChangesSelected()); }
InternalLoadServerHistoryScopeAsync(string scopeId, SyncContext context, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { var scopeBuilder = this.GetScopeBuilder(this.Options.ScopeInfoTableName); using var command = scopeBuilder.GetCommandAsync(DbScopeCommandType.GetServerHistoryScopeInfo, connection, transaction); if (command == null) { return(context, null); } DbSyncAdapter.SetParameterValue(command, "sync_scope_id", scopeId); DbSyncAdapter.SetParameterValue(command, "sync_scope_name", context.ScopeName); await this.InterceptAsync(new DbCommandArgs(context, command, connection, transaction), progress, cancellationToken).ConfigureAwait(false); using DbDataReader reader = await command.ExecuteReaderAsync().ConfigureAwait(false); ServerHistoryScopeInfo serverHistoryScopeInfo = null; if (reader.Read()) { serverHistoryScopeInfo = InternalReadServerHistoryScopeInfo(reader); } reader.Close(); if (serverHistoryScopeInfo.Schema != null) { serverHistoryScopeInfo.Schema.EnsureSchema(); } command.Dispose(); return(context, serverHistoryScopeInfo); }
/// <summary> /// Gets a batch of changes to synchronize when given batch size, /// destination knowledge, and change data retriever parameters. /// </summary> /// <returns>A DbSyncContext object that will be used to retrieve the modified data.</returns> internal virtual async Task <(SyncContext, BatchInfo, DatabaseChangesSelected)> InternalGetChangesAsync( SyncContext context, MessageGetChangesBatch message, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { // batch info containing changes BatchInfo batchInfo; // Statistics about changes that are selected DatabaseChangesSelected changesSelected; if (context.SyncWay == SyncWay.Upload && context.SyncType == SyncType.Reinitialize) { (batchInfo, changesSelected) = await this.InternalGetEmptyChangesAsync(message).ConfigureAwait(false); return(context, batchInfo, changesSelected); } // Call interceptor await this.InterceptAsync(new DatabaseChangesSelectingArgs(context, message, connection, transaction), cancellationToken).ConfigureAwait(false); // create local directory if (message.BatchSize > 0 && !string.IsNullOrEmpty(message.BatchDirectory) && !Directory.Exists(message.BatchDirectory)) { Directory.CreateDirectory(message.BatchDirectory); } changesSelected = new DatabaseChangesSelected(); // numbers of batch files generated var batchIndex = 0; // Check if we are in batch mode var isBatch = message.BatchSize > 0; // Create a batch info in memory (if !isBatch) or serialized on disk (if isBatch) // batchinfo generate a schema clone with scope columns if needed batchInfo = new BatchInfo(!isBatch, message.Schema, message.BatchDirectory); // Clean SyncSet, we will add only tables we need in the batch info var changesSet = new SyncSet(); var cptSyncTable = 0; var currentProgress = context.ProgressPercentage; foreach (var syncTable in message.Schema.Tables) { // tmp count of table for report progress pct cptSyncTable++; // Only table schema is replicated, no datas are applied if (syncTable.SyncDirection == SyncDirection.None) { continue; } // if we are in upload stage, so check if table is not download only if (context.SyncWay == SyncWay.Upload && syncTable.SyncDirection == SyncDirection.DownloadOnly) { continue; } // if we are in download stage, so check if table is not download only if (context.SyncWay == SyncWay.Download && syncTable.SyncDirection == SyncDirection.UploadOnly) { continue; } // Get Command var selectIncrementalChangesCommand = await this.GetSelectChangesCommandAsync(context, syncTable, message.Setup, message.IsNew, connection, transaction); // Set parameters this.SetSelectChangesCommonParameters(context, syncTable, message.ExcludingScopeId, message.IsNew, message.LastTimestamp, selectIncrementalChangesCommand); // launch interceptor if any var args = new TableChangesSelectingArgs(context, syncTable, selectIncrementalChangesCommand, connection, transaction); await this.InterceptAsync(args, cancellationToken).ConfigureAwait(false); if (!args.Cancel && args.Command != null) { // Statistics var tableChangesSelected = new TableChangesSelected(syncTable.TableName, syncTable.SchemaName); // Create a chnages table with scope columns var changesSetTable = DbSyncAdapter.CreateChangesTable(message.Schema.Tables[syncTable.TableName, syncTable.SchemaName], changesSet); // Get the reader using var dataReader = await args.Command.ExecuteReaderAsync().ConfigureAwait(false); // memory size total double rowsMemorySize = 0L; while (dataReader.Read()) { // Create a row from dataReader var row = CreateSyncRowFromReader(dataReader, changesSetTable); // Add the row to the changes set changesSetTable.Rows.Add(row); // Set the correct state to be applied if (row.RowState == DataRowState.Deleted) { tableChangesSelected.Deletes++; } else if (row.RowState == DataRowState.Modified) { tableChangesSelected.Upserts++; } // calculate row size if in batch mode if (isBatch) { var fieldsSize = ContainerTable.GetRowSizeFromDataRow(row.ToArray()); var finalFieldSize = fieldsSize / 1024d; if (finalFieldSize > message.BatchSize) { throw new RowOverSizedException(finalFieldSize.ToString()); } // Calculate the new memory size rowsMemorySize += finalFieldSize; // Next line if we don't reach the batch size yet. if (rowsMemorySize <= message.BatchSize) { continue; } // Check interceptor var batchTableChangesSelectedArgs = new TableChangesSelectedArgs(context, changesSetTable, tableChangesSelected, connection, transaction); await this.InterceptAsync(batchTableChangesSelectedArgs, cancellationToken).ConfigureAwait(false); // add changes to batchinfo await batchInfo.AddChangesAsync(changesSet, batchIndex, false, message.SerializerFactory, this).ConfigureAwait(false); // increment batch index batchIndex++; // we know the datas are serialized here, so we can flush the set changesSet.Clear(); // Recreate an empty ContainerSet and a ContainerTable changesSet = new SyncSet(); changesSetTable = DbSyncAdapter.CreateChangesTable(message.Schema.Tables[syncTable.TableName, syncTable.SchemaName], changesSet); // Init the row memory size rowsMemorySize = 0L; } } dataReader.Close(); // We don't report progress if no table changes is empty, to limit verbosity if (tableChangesSelected.Deletes > 0 || tableChangesSelected.Upserts > 0) { changesSelected.TableChangesSelected.Add(tableChangesSelected); } // even if no rows raise the interceptor var tableChangesSelectedArgs = new TableChangesSelectedArgs(context, changesSetTable, tableChangesSelected, connection, transaction); await this.InterceptAsync(tableChangesSelectedArgs, cancellationToken).ConfigureAwait(false); context.ProgressPercentage = currentProgress + (cptSyncTable * 0.2d / message.Schema.Tables.Count); // only raise report progress if we have something if (tableChangesSelectedArgs.TableChangesSelected.TotalChanges > 0) { this.ReportProgress(context, progress, tableChangesSelectedArgs); } } } // We are in batch mode, and we are at the last batchpart info // Even if we don't have rows inside, we return the changesSet, since it contains at least schema if (changesSet != null && changesSet.HasTables && changesSet.HasRows) { await batchInfo.AddChangesAsync(changesSet, batchIndex, true, message.SerializerFactory, this).ConfigureAwait(false); } //Set the total rows count contained in the batch info batchInfo.RowsCount = changesSelected.TotalChangesSelected; // Check the last index as the last batch batchInfo.EnsureLastBatch(); // Raise database changes selected if (changesSelected.TotalChangesSelected > 0 || changesSelected.TotalChangesSelectedDeletes > 0 || changesSelected.TotalChangesSelectedUpdates > 0) { var databaseChangesSelectedArgs = new DatabaseChangesSelectedArgs(context, message.LastTimestamp, batchInfo, changesSelected, connection); this.ReportProgress(context, progress, databaseChangesSelectedArgs); await this.InterceptAsync(databaseChangesSelectedArgs, cancellationToken).ConfigureAwait(false); } return(context, batchInfo, changesSelected); }
/// <summary> /// update configuration object with tables desc from server database /// </summary> public virtual async Task <SyncContext> CreateSnapshotAsync(SyncContext context, SyncSet schema, DbConnection connection, DbTransaction transaction, string batchDirectory, int batchSize, long remoteClientTimestamp, CancellationToken cancellationToken, IProgress <ProgressArgs> progress = null) { // create local directory if (!Directory.Exists(batchDirectory)) { Directory.CreateDirectory(batchDirectory); } // numbers of batch files generated var batchIndex = 0; // create the in memory changes set var changesSet = new SyncSet(); // Create a Schema set without readonly tables, attached to memory changes foreach (var table in schema.Tables) { DbSyncAdapter.CreateChangesTable(schema.Tables[table.TableName, table.SchemaName], changesSet); } var sb = new StringBuilder(); var underscore = ""; if (context.Parameters != null) { foreach (var p in context.Parameters.OrderBy(p => p.Name)) { var cleanValue = new string(p.Value.ToString().Where(char.IsLetterOrDigit).ToArray()); var cleanName = new string(p.Name.Where(char.IsLetterOrDigit).ToArray()); sb.Append($"{underscore}{cleanName}_{cleanValue}"); underscore = "_"; } } var directoryName = sb.ToString(); directoryName = string.IsNullOrEmpty(directoryName) ? "ALL" : directoryName; var directoryFullPath = Path.Combine(batchDirectory, directoryName); if (Directory.Exists(directoryFullPath)) { Directory.Delete(directoryFullPath, true); } // batchinfo generate a schema clone with scope columns if needed var batchInfo = new BatchInfo(false, changesSet, batchDirectory, directoryName); // Clear tables, we will add only the ones we need in the batch info changesSet.Clear(); foreach (var syncTable in schema.Tables) { var tableBuilder = this.GetTableBuilder(syncTable); var syncAdapter = tableBuilder.CreateSyncAdapter(connection, transaction); // raise before event context.SyncStage = SyncStage.TableChangesSelecting; var tableChangesSelectingArgs = new TableChangesSelectingArgs(context, syncTable.TableName, connection, transaction); // launch interceptor if any await this.InterceptAsync(tableChangesSelectingArgs).ConfigureAwait(false); // Get Select initialize changes command var selectIncrementalChangesCommand = this.GetSelectChangesCommand(context, syncAdapter, syncTable, true); // Set parameters this.SetSelectChangesCommonParameters(context, syncTable, null, true, 0, selectIncrementalChangesCommand); // Get the reader using (var dataReader = selectIncrementalChangesCommand.ExecuteReader()) { // memory size total double rowsMemorySize = 0L; // Create a chnages table with scope columns var changesSetTable = DbSyncAdapter.CreateChangesTable(schema.Tables[syncTable.TableName, syncTable.SchemaName], changesSet); while (dataReader.Read()) { // Create a row from dataReader var row = CreateSyncRowFromReader(dataReader, changesSetTable); // Add the row to the changes set changesSetTable.Rows.Add(row); var fieldsSize = ContainerTable.GetRowSizeFromDataRow(row.ToArray()); var finalFieldSize = fieldsSize / 1024d; if (finalFieldSize > batchSize) { throw new RowOverSizedException(finalFieldSize.ToString()); } // Calculate the new memory size rowsMemorySize += finalFieldSize; // Next line if we don't reach the batch size yet. if (rowsMemorySize <= batchSize) { continue; } // add changes to batchinfo batchInfo.AddChanges(changesSet, batchIndex, false); // increment batch index batchIndex++; // we know the datas are serialized here, so we can flush the set changesSet.Clear(); // Recreate an empty ContainerSet and a ContainerTable changesSet = new SyncSet(); changesSetTable = DbSyncAdapter.CreateChangesTable(schema.Tables[syncTable.TableName, syncTable.SchemaName], changesSet); // Init the row memory size rowsMemorySize = 0L; } } selectIncrementalChangesCommand.Dispose(); } if (changesSet != null && changesSet.HasTables) { batchInfo.AddChanges(changesSet, batchIndex, true); } // Check the last index as the last batch batchInfo.EnsureLastBatch(); batchInfo.Timestamp = remoteClientTimestamp; // Serialize on disk. var jsonConverter = new JsonConverter <BatchInfo>(); var summaryFileName = Path.Combine(directoryFullPath, "summary.json"); using (var f = new FileStream(summaryFileName, FileMode.CreateNew, FileAccess.ReadWrite)) { var bytes = jsonConverter.Serialize(batchInfo); f.Write(bytes, 0, bytes.Length); } return(context); }
InternalGetSnapshotAsync(ServerScopeInfo serverScopeInfo, SyncContext context, DbConnection connection = default, DbTransaction transaction = default, CancellationToken cancellationToken = default, IProgress <ProgressArgs> progress = null) { try { await using var runner = await this.GetConnectionAsync(context, SyncMode.Reading, SyncStage.ScopeLoading, connection, transaction, cancellationToken, progress).ConfigureAwait(false); // Get context or create a new one var changesSelected = new DatabaseChangesSelected(); BatchInfo serverBatchInfo = null; if (string.IsNullOrEmpty(this.Options.SnapshotsDirectory)) { return(context, 0, null, changesSelected); } //Direction set to Download context.SyncWay = SyncWay.Download; if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Get Schema from remote provider if no schema passed from args if (serverScopeInfo.Schema == null) { (context, serverScopeInfo) = await this.InternalGetServerScopeInfoAsync(context, serverScopeInfo.Setup, false, runner.Connection, runner.Transaction, runner.CancellationToken, runner.Progress).ConfigureAwait(false); } // When we get the changes from server, we create the batches if it's requested by the client // the batch decision comes from batchsize from client var(rootDirectory, nameDirectory) = await this.InternalGetSnapshotDirectoryPathAsync(serverScopeInfo.Name, context.Parameters, runner.CancellationToken, runner.Progress).ConfigureAwait(false); if (!string.IsNullOrEmpty(rootDirectory)) { var directoryFullPath = Path.Combine(rootDirectory, nameDirectory); // if no snapshot present, just return null value. if (Directory.Exists(directoryFullPath)) { // Serialize on disk. var jsonConverter = new Serialization.JsonConverter <BatchInfo>(); var summaryFileName = Path.Combine(directoryFullPath, "summary.json"); using (var fs = new FileStream(summaryFileName, FileMode.Open, FileAccess.Read)) { serverBatchInfo = await jsonConverter.DeserializeAsync(fs).ConfigureAwait(false); } // Create the schema changeset var changesSet = new SyncSet(); // Create a Schema set without readonly columns, attached to memory changes foreach (var table in serverScopeInfo.Schema.Tables) { DbSyncAdapter.CreateChangesTable(serverScopeInfo.Schema.Tables[table.TableName, table.SchemaName], changesSet); // Get all stats about this table var bptis = serverBatchInfo.BatchPartsInfo.SelectMany(bpi => bpi.Tables.Where(t => { var sc = SyncGlobalization.DataSourceStringComparison; var sn = t.SchemaName == null ? string.Empty : t.SchemaName; var otherSn = table.SchemaName == null ? string.Empty : table.SchemaName; return(table.TableName.Equals(t.TableName, sc) && sn.Equals(otherSn, sc)); })); if (bptis != null) { // Statistics var tableChangesSelected = new TableChangesSelected(table.TableName, table.SchemaName) { // we are applying a snapshot where it can't have any deletes, obviously Upserts = bptis.Sum(bpti => bpti.RowsCount) }; if (tableChangesSelected.Upserts > 0) { changesSelected.TableChangesSelected.Add(tableChangesSelected); } } } serverBatchInfo.SanitizedSchema = changesSet; } } if (serverBatchInfo == null) { return(context, 0, null, changesSelected); } await runner.CommitAsync().ConfigureAwait(false); return(context, serverBatchInfo.Timestamp, serverBatchInfo, changesSelected); } catch (Exception ex) { throw GetSyncError(context, ex); } }
/// <summary> /// Handle a conflict /// The int returned is the conflict count I need /// </summary> /// changeApplicationAction, conflictCount, resolvedRow, conflictApplyInt internal async Task <(int conflictResolvedCount, SyncRow resolvedRow, int rowAppliedCount)> HandleConflictAsync( Guid localScopeId, Guid senderScopeId, DbSyncAdapter syncAdapter, SyncContext context, SyncConflict conflict, ConflictResolutionPolicy policy, long lastTimestamp, DbConnection connection, DbTransaction transaction) { SyncRow finalRow; ApplyAction conflictApplyAction; int rowAppliedCount = 0; (conflictApplyAction, finalRow) = await this.GetConflictActionAsync(context, conflict, policy, connection, transaction).ConfigureAwait(false); // Conflict rollbacked by user if (conflictApplyAction == ApplyAction.Rollback) { throw new RollbackException("Rollback action taken on conflict"); } // 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(0, finalRow, 0); } // if we have a merge action, we apply the row on the server if (isMergeAction) { // if merge, we update locally the row and let the update_scope_id set to null var isUpdated = await syncAdapter.ApplyUpdateAsync(row, lastTimestamp, null, true); // We don't update metadatas so the row is updated (on server side) // and is mark as updated locally. // and will be returned back to sender, since it's a merge, and we need it on the client if (!isUpdated) { throw new Exception("Can't update the merge row."); } } finalRow = isMergeAction ? row : conflict.LocalRow; // We don't do anything, since we let the original row. so we resolved one conflict but applied no rows return(conflictResolvedCount : 1, finalRow, rowAppliedCount : 0); } // We gonna apply with force the line if (conflictApplyAction == ApplyAction.RetryWithForceWrite) { // TODO : Should Raise an error ? if (conflict.RemoteRow == null) { return(0, finalRow, 0); } bool operationComplete = false; switch (conflict.Type) { // Remote source has row, Local don't have the row, so insert it case ConflictType.RemoteExistsLocalExists: case ConflictType.RemoteExistsLocalNotExists: case ConflictType.RemoteExistsLocalIsDeleted: case ConflictType.UniqueKeyConstraint: operationComplete = await syncAdapter.ApplyUpdateAsync(conflict.RemoteRow, lastTimestamp, senderScopeId, true); rowAppliedCount = 1; break; // Conflict, but both have delete the row, so nothing to do case ConflictType.RemoteIsDeletedLocalIsDeleted: case ConflictType.RemoteIsDeletedLocalNotExists: operationComplete = true; rowAppliedCount = 0; break; // The remote has delete the row, and local has insert or update it // So delete the local row case ConflictType.RemoteIsDeletedLocalExists: operationComplete = await syncAdapter.ApplyDeleteAsync(conflict.RemoteRow, lastTimestamp, senderScopeId, true); rowAppliedCount = 1; break; case ConflictType.RemoteCleanedupDeleteLocalUpdate: case ConflictType.ErrorsOccurred: return(0, finalRow, 0); } finalRow = conflict.RemoteRow; //After a force update, there is a problem, so raise exception if (!operationComplete) { finalRow = null; return(0, finalRow, rowAppliedCount); } return(1, finalRow, rowAppliedCount); } return(0, finalRow, 0); }
private async Task <(int rowsAppliedCount, int conflictsResolvedCount, int syncErrorsCount)> ResolveConflictsAsync(SyncContext context, Guid localScopeId, Guid senderScopeId, DbSyncAdapter syncAdapter, List <SyncConflict> conflicts, MessageApplyChanges message, DbConnection connection, DbTransaction transaction) { int rowsAppliedCount = 0; int conflictsResolvedCount = 0; int syncErrorsCount = 0; // If conflicts occured // Eventuall, conflicts are resolved on server side. if (conflicts == null || conflicts.Count <= 0) { return(rowsAppliedCount, conflictsResolvedCount, syncErrorsCount); } foreach (var conflict in conflicts) { var fromScopeLocalTimeStamp = message.LastTimestamp; this.Orchestrator.logger.LogDebug(SyncEventsId.ResolveConflicts, conflict); this.Orchestrator.logger.LogDebug(SyncEventsId.ResolveConflicts, new { LocalScopeId = localScopeId, SenderScopeId = senderScopeId, FromScopeLocalTimeStamp = fromScopeLocalTimeStamp, message.Policy });; var(conflictResolvedCount, resolvedRow, rowAppliedCount) = await this.HandleConflictAsync(localScopeId, senderScopeId, syncAdapter, context, conflict, message.Policy, fromScopeLocalTimeStamp, connection, transaction).ConfigureAwait(false); if (resolvedRow != null) { conflictsResolvedCount += conflictResolvedCount; rowsAppliedCount += rowAppliedCount; } } return(rowsAppliedCount, conflictsResolvedCount, syncErrorsCount); }
/// <summary> /// Handle a conflict /// The int returned is the conflict count I need /// </summary> private async Task <(int conflictResolvedCount, SyncRow resolvedRow, int rowAppliedCount)> HandleConflictAsync( Guid localScopeId, Guid senderScopeId, DbSyncAdapter syncAdapter, SyncContext context, SyncRow conflictRow, SyncTable schemaChangesTable, ConflictResolutionPolicy policy, long?lastTimestamp, DbConnection connection, DbTransaction transaction) { SyncRow finalRow; SyncRow localRow; ConflictType conflictType; ApplyAction conflictApplyAction; int rowAppliedCount = 0; Guid? nullableSenderScopeId; (conflictApplyAction, conflictType, localRow, finalRow, nullableSenderScopeId) = await this.GetConflictActionAsync(context, localScopeId, syncAdapter, conflictRow, schemaChangesTable, policy, senderScopeId, connection, transaction).ConfigureAwait(false); // Conflict rollbacked by user if (conflictApplyAction == ApplyAction.Rollback) { throw new RollbackException("Rollback action taken on conflict"); } // Local provider wins, update metadata if (conflictApplyAction == ApplyAction.Continue) { var isMergeAction = finalRow != null; var row = isMergeAction ? finalRow : localRow; // Conflict on a line that is not present on the datasource if (row == null) { return(conflictResolvedCount : 1, finalRow, rowAppliedCount : 0); } // if we have a merge action, we apply the row on the server if (isMergeAction) { // if merge, we update locally the row and let the update_scope_id set to null var isUpdated = await this.InternalApplyConflictUpdateAsync(context, syncAdapter, row, lastTimestamp, null, true, connection, transaction).ConfigureAwait(false); // We don't update metadatas so the row is updated (on server side) // and is mark as updated locally. // and will be returned back to sender, since it's a merge, and we need it on the client if (!isUpdated) { throw new Exception("Can't update the merge row."); } } finalRow = isMergeAction ? row : localRow; // We don't do anything, since we let the original row. so we resolved one conflict but applied no rows return(conflictResolvedCount : 1, finalRow, rowAppliedCount : 0); } // We gonna apply with force the line if (conflictApplyAction == ApplyAction.RetryWithForceWrite) { // TODO : Should Raise an error ? if (conflictRow == null) { return(0, finalRow, 0); } bool operationComplete = false; switch (conflictType) { // Remote source has row, Local don't have the row, so insert it case ConflictType.RemoteExistsLocalExists: operationComplete = await this.InternalApplyConflictUpdateAsync(context, syncAdapter, conflictRow, lastTimestamp, nullableSenderScopeId, true, connection, transaction).ConfigureAwait(false); rowAppliedCount = 1; break; case ConflictType.RemoteExistsLocalNotExists: case ConflictType.RemoteExistsLocalIsDeleted: case ConflictType.UniqueKeyConstraint: operationComplete = await this.InternalApplyConflictUpdateAsync(context, syncAdapter, conflictRow, lastTimestamp, nullableSenderScopeId, true, connection, transaction).ConfigureAwait(false); rowAppliedCount = 1; break; // Conflict, but both have delete the row, so just update the metadata to the right winner case ConflictType.RemoteIsDeletedLocalIsDeleted: operationComplete = await this.InternalUpdateMetadatasAsync(context, syncAdapter, conflictRow, nullableSenderScopeId, true, connection, transaction).ConfigureAwait(false); rowAppliedCount = 0; break; // The row does not exists locally, and since it's coming from a deleted state, we can forget it case ConflictType.RemoteIsDeletedLocalNotExists: operationComplete = true; rowAppliedCount = 0; break; // The remote has delete the row, and local has insert or update it // So delete the local row case ConflictType.RemoteIsDeletedLocalExists: operationComplete = await this.InternalApplyConflictDeleteAsync(context, syncAdapter, conflictRow, lastTimestamp, nullableSenderScopeId, true, connection, transaction); // Conflict, but both have delete the row, so just update the metadata to the right winner if (!operationComplete) { operationComplete = await this.InternalUpdateMetadatasAsync(context, syncAdapter, conflictRow, nullableSenderScopeId, true, connection, transaction); rowAppliedCount = 0; } else { rowAppliedCount = 1; } break; case ConflictType.ErrorsOccurred: return(0, finalRow, 0); } finalRow = conflictRow; //After a force update, there is a problem, so raise exception if (!operationComplete) { throw new UnknownException("Force update should always work.. contact the author :)"); } return(1, finalRow, rowAppliedCount); } return(0, finalRow, 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); }
/// <summary> /// A conflict has occured, we try to ask for the solution to the user /// </summary> private async Task <(ApplyAction, ConflictType, SyncRow, SyncRow, Guid?)> GetConflictActionAsync(SyncContext context, Guid localScopeId, DbSyncAdapter syncAdapter, SyncRow conflictRow, SyncTable schemaChangesTable, ConflictResolutionPolicy policy, Guid senderScopeId, DbConnection connection, DbTransaction transaction = null, CancellationToken cancellationToken = default) { // default action var resolution = policy == ConflictResolutionPolicy.ClientWins ? ConflictResolution.ClientWins : ConflictResolution.ServerWins; // if ConflictAction is ServerWins or MergeRow it's Ok to set to Continue var action = ApplyAction.Continue; // check the interceptor var interceptor = this.interceptors.GetInterceptor <ApplyChangesFailedArgs>(); SyncRow finalRow = null; SyncRow localRow = null; Guid? finalSenderScopeId = senderScopeId; // default conflict type ConflictType conflictType = conflictRow.RowState == DataRowState.Deleted ? ConflictType.RemoteIsDeletedLocalExists : ConflictType.RemoteExistsLocalExists; // if is not empty, get the conflict and intercept if (!interceptor.IsEmpty) { // Get the localRow localRow = await this.InternalGetConflictRowAsync(context, syncAdapter, localScopeId, conflictRow, schemaChangesTable, connection, transaction).ConfigureAwait(false); // Get the conflict var conflict = this.GetConflict(conflictRow, localRow); // Interceptor var arg = new ApplyChangesFailedArgs(context, conflict, resolution, senderScopeId, connection, transaction); await this.InterceptAsync(arg, cancellationToken).ConfigureAwait(false); resolution = arg.Resolution; finalRow = arg.Resolution == ConflictResolution.MergeRow ? arg.FinalRow : null; finalSenderScopeId = arg.SenderScopeId; conflictType = arg.Conflict.Type; } // Change action only if we choose ClientWins or Rollback. // for ServerWins or MergeRow, action is Continue if (resolution == ConflictResolution.ClientWins) { action = ApplyAction.RetryWithForceWrite; } else if (resolution == ConflictResolution.Rollback) { action = ApplyAction.Rollback; } // returning the action to take, and actually the finalRow if action is set to Merge return(action, conflictType, localRow, finalRow, finalSenderScopeId); }