/// <summary> /// Apply changes : Delete / Insert / Update /// the fromScope is local client scope when this method is called from server /// the fromScope is server scope when this method is called from client /// </summary> public virtual async Task <(SyncContext, DatabaseChangesApplied)> ApplyChangesAsync(SyncContext context, MessageApplyChanges message) { var changeApplicationAction = ChangeApplicationAction.Continue; DbTransaction applyTransaction = null; DbConnection connection = null; var changesApplied = new DatabaseChangesApplied(); try { using (connection = this.CreateConnection()) { await connection.OpenAsync(); // Create a transaction applyTransaction = connection.BeginTransaction(); context.SyncStage = SyncStage.DatabaseChangesApplying; // Launch any interceptor if available await this.InterceptAsync(new DatabaseChangesApplyingArgs(context, connection, applyTransaction)); // Disable check constraints if (this.Options.DisableConstraintsOnApplyChanges) { changeApplicationAction = this.DisableConstraints(context, message.Schema, connection, applyTransaction, message.FromScope); } // ----------------------------------------------------- // 0) Check if we are in a reinit mode // ----------------------------------------------------- if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal) { changeApplicationAction = this.ResetInternal(context, message.Schema, connection, applyTransaction, message.FromScope); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during reset tables", context.SyncStage, SyncExceptionType.Rollback); } } // ----------------------------------------------------- // 1) Applying deletes. Do not apply deletes if we are in a new database // ----------------------------------------------------- if (!message.FromScope.IsNewScope) { // for delete we must go from Up to Down foreach (var table in message.Schema.Tables.Reverse()) { changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection, applyTransaction, DmRowState.Deleted, changesApplied); } // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { RaiseRollbackException(context, "Rollback during applying deletes"); } } // ----------------------------------------------------- // 2) Applying Inserts and Updates. Apply in table order // ----------------------------------------------------- foreach (var table in message.Schema.Tables) { changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection, applyTransaction, DmRowState.Added, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { RaiseRollbackException(context, "Rollback during applying inserts"); } changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection, applyTransaction, DmRowState.Modified, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { RaiseRollbackException(context, "Rollback during applying updates"); } } // Progress & Interceptor context.SyncStage = SyncStage.DatabaseChangesApplied; var databaseChangesAppliedArgs = new DatabaseChangesAppliedArgs(context, connection, applyTransaction); this.ReportProgress(context, databaseChangesAppliedArgs, connection, applyTransaction); await this.InterceptAsync(databaseChangesAppliedArgs); // Re enable check constraints if (this.Options.DisableConstraintsOnApplyChanges) { changeApplicationAction = this.EnableConstraints(context, message.Schema, connection, applyTransaction, message.FromScope); } applyTransaction.Commit(); return(context, changesApplied); } } catch (SyncException) { throw; } catch (Exception ex) { throw new SyncException(ex, SyncStage.TableChangesApplying); } finally { if (applyTransaction != null) { applyTransaction.Dispose(); applyTransaction = null; } if (connection != null && connection.State == ConnectionState.Open) { connection.Close(); } if (message.Changes != null) { message.Changes.Clear(this.Options.CleanMetadatas); } } }
InternalApplyChangesAsync(SyncContext context, MessageApplyChanges message, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { // call interceptor await this.InterceptAsync(new DatabaseChangesApplyingArgs(context, message, connection, transaction), cancellationToken).ConfigureAwait(false); var changesApplied = new DatabaseChangesApplied(); // Check if we have some data available var hasChanges = message.Changes.HasData(); // if we have changes or if we are in re init mode if (hasChanges || context.SyncType != SyncType.Normal) { // Disable check constraints // Because Sqlite does not support "PRAGMA foreign_keys=OFF" Inside a transaction // Report this disabling constraints brefore opening a transaction if (message.DisableConstraintsOnApplyChanges) { foreach (var table in message.Schema.Tables.Reverse()) { await this.InternalDisableConstraintsAsync(context, this.GetSyncAdapter(table, message.Setup), connection, transaction).ConfigureAwait(false); } } // ----------------------------------------------------- // 0) Check if we are in a reinit mode (Check also SyncWay to be sure we don't reset tables on server) // ----------------------------------------------------- if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal) { foreach (var table in message.Schema.Tables) { await this.InternalResetTableAsync(context, this.GetSyncAdapter(table, message.Setup), connection, transaction).ConfigureAwait(false); } } // ----------------------------------------------------- // 1) Applying deletes. Do not apply deletes if we are in a new database // ----------------------------------------------------- if (!message.IsNew && hasChanges) { foreach (var table in message.Schema.Tables.Reverse()) { await this.InternalApplyTableChangesAsync(context, table, message, connection, transaction, DataRowState.Deleted, changesApplied, cancellationToken, progress).ConfigureAwait(false); } } // ----------------------------------------------------- // 2) Applying Inserts and Updates. Apply in table order // ----------------------------------------------------- if (hasChanges) { foreach (var table in message.Schema.Tables) { await this.InternalApplyTableChangesAsync(context, table, message, connection, transaction, DataRowState.Modified, changesApplied, cancellationToken, progress).ConfigureAwait(false); } } // Re enable check constraints if (message.DisableConstraintsOnApplyChanges) { foreach (var table in message.Schema.Tables) { await this.InternalEnableConstraintsAsync(context, this.GetSyncAdapter(table, message.Setup), connection, transaction).ConfigureAwait(false); } } } // Before cleaning, check if we are not applying changes from a snapshotdirectory var cleanFolder = message.CleanFolder; if (cleanFolder) { cleanFolder = await this.InternalCanCleanFolderAsync(context, message.Changes, cancellationToken, progress).ConfigureAwait(false); } // clear the changes because we don't need them anymore message.Changes.Clear(cleanFolder); var databaseChangesAppliedArgs = new DatabaseChangesAppliedArgs(context, changesApplied, connection, transaction); await this.InterceptAsync(databaseChangesAppliedArgs, cancellationToken).ConfigureAwait(false); this.ReportProgress(context, progress, databaseChangesAppliedArgs); return(context, changesApplied); }
InternalApplyChangesAsync(IScopeInfo scopeInfo, SyncContext context, MessageApplyChanges message, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { context.SyncStage = SyncStage.ChangesApplying; // call interceptor var databaseChangesApplyingArgs = new DatabaseChangesApplyingArgs(context, message, connection, transaction); await this.InterceptAsync(databaseChangesApplyingArgs, progress, cancellationToken).ConfigureAwait(false); var changesApplied = new DatabaseChangesApplied(); // Check if we have some data available var hasChanges = message.BatchInfo.HasData(); // if we have changes or if we are in re init mode if (hasChanges || context.SyncType != SyncType.Normal) { var schemaTables = message.Schema.Tables.SortByDependencies(tab => tab.GetRelations().Select(r => r.GetParentTable())); // Disable check constraints // Because Sqlite does not support "PRAGMA foreign_keys=OFF" Inside a transaction // Report this disabling constraints brefore opening a transaction if (message.DisableConstraintsOnApplyChanges) { foreach (var table in schemaTables) { var syncAdapter = this.GetSyncAdapter(table, scopeInfo); context = await this.InternalDisableConstraintsAsync(scopeInfo, context, syncAdapter, connection, transaction).ConfigureAwait(false); } } // ----------------------------------------------------- // 0) Check if we are in a reinit mode (Check also SyncWay to be sure we don't reset tables on server, then check if we don't have already applied a snapshot) // ----------------------------------------------------- if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal && !message.SnapshoteApplied) { foreach (var table in schemaTables.Reverse()) { var syncAdapter = this.GetSyncAdapter(table, scopeInfo); context = await this.InternalResetTableAsync(scopeInfo, context, syncAdapter, connection, transaction).ConfigureAwait(false); } } // Trying to change order (from deletes-upserts to upserts-deletes) // see https://github.com/Mimetis/Dotmim.Sync/discussions/453#discussioncomment-380530 // ----------------------------------------------------- // 1) Applying Inserts and Updates. Apply in table order // ----------------------------------------------------- if (hasChanges) { foreach (var table in schemaTables) { context = await this.InternalApplyTableChangesAsync(scopeInfo, context, table, message, connection, transaction, DataRowState.Modified, changesApplied, cancellationToken, progress).ConfigureAwait(false); } } // ----------------------------------------------------- // 2) Applying Deletes. Do not apply deletes if we are in a new database // ----------------------------------------------------- if (!message.IsNew && hasChanges) { foreach (var table in schemaTables.Reverse()) { context = await this.InternalApplyTableChangesAsync(scopeInfo, context, table, message, connection, transaction, DataRowState.Deleted, changesApplied, cancellationToken, progress).ConfigureAwait(false); } } // Re enable check constraints if (message.DisableConstraintsOnApplyChanges) { foreach (var table in schemaTables) { context = await this.InternalEnableConstraintsAsync(scopeInfo, context, this.GetSyncAdapter(table, scopeInfo), connection, transaction).ConfigureAwait(false); } } // Dispose data message.BatchInfo.Clear(false); } // Before cleaning, check if we are not applying changes from a snapshotdirectory var cleanFolder = message.CleanFolder; if (cleanFolder) { cleanFolder = await this.InternalCanCleanFolderAsync(scopeInfo.Name, context.Parameters, message.BatchInfo, cancellationToken, progress).ConfigureAwait(false); } // clear the changes because we don't need them anymore message.BatchInfo.Clear(cleanFolder); var databaseChangesAppliedArgs = new DatabaseChangesAppliedArgs(context, changesApplied, connection, transaction); await this.InterceptAsync(databaseChangesAppliedArgs, progress, cancellationToken).ConfigureAwait(false); return(context, changesApplied); }
ApplyChangesAsync(ScopeInfo scope, SyncSet schema, BatchInfo serverBatchInfo, long clientTimestamp, long remoteClientTimestamp, ConflictResolutionPolicy policy, CancellationToken cancellationToken = default, IProgress <ProgressArgs> progress = null) { if (!this.StartTime.HasValue) { this.StartTime = DateTime.UtcNow; } // Get context or create a new one var ctx = this.GetContext(); DatabaseChangesApplied clientChangesApplied = null; using (var connection = this.Provider.CreateConnection()) { try { ctx.SyncStage = SyncStage.ChangesApplying; // Open connection await this.OpenConnectionAsync(connection, cancellationToken).ConfigureAwait(false); // Create a transaction using (var transaction = connection.BeginTransaction()) { await this.InterceptAsync(new TransactionOpenedArgs(ctx, connection, transaction), cancellationToken).ConfigureAwait(false); // lastSyncTS : apply lines only if they are not modified since last client sync var lastSyncTS = scope.LastSyncTimestamp; // isNew : if IsNew, don't apply deleted rows from server var isNew = scope.IsNewScope; // We are in downloading mode ctx.SyncWay = SyncWay.Download; // Create the message containing everything needed to apply changes var applyChanges = new MessageApplyChanges(scope.Id, Guid.Empty, isNew, lastSyncTS, schema, this.Setup, policy, this.Options.DisableConstraintsOnApplyChanges, this.Options.UseBulkOperations, this.Options.CleanMetadatas, this.Options.CleanFolder, serverBatchInfo); // call interceptor await this.InterceptAsync(new DatabaseChangesApplyingArgs(ctx, applyChanges, connection, transaction), cancellationToken).ConfigureAwait(false); // Call apply changes on provider (ctx, clientChangesApplied) = await this.Provider.ApplyChangesAsync(ctx, applyChanges, connection, transaction, cancellationToken, progress).ConfigureAwait(false); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // check if we need to delete metadatas if (this.Options.CleanMetadatas && clientChangesApplied.TotalAppliedChanges > 0) { await this.Provider.DeleteMetadatasAsync(ctx, schema, this.Setup, lastSyncTS, connection, transaction, cancellationToken, progress); } // now the sync is complete, remember the time this.CompleteTime = DateTime.UtcNow; // generate the new scope item scope.IsNewScope = false; scope.LastSync = this.CompleteTime; scope.LastSyncTimestamp = clientTimestamp; scope.LastServerSyncTimestamp = remoteClientTimestamp; scope.LastSyncDuration = this.CompleteTime.Value.Subtract(this.StartTime.Value).Ticks; scope.Setup = this.Setup; // Write scopes locally ctx = await this.Provider.WriteClientScopeAsync(ctx, this.Options.ScopeInfoTableName, scope, connection, transaction, cancellationToken, progress).ConfigureAwait(false); await this.InterceptAsync(new TransactionCommitArgs(ctx, connection, transaction), cancellationToken).ConfigureAwait(false); transaction.Commit(); } ctx.SyncStage = SyncStage.ChangesApplied; await this.CloseConnectionAsync(connection, cancellationToken).ConfigureAwait(false); this.logger.LogInformation(SyncEventsId.ApplyChanges, clientChangesApplied); var databaseChangesAppliedArgs = new DatabaseChangesAppliedArgs(ctx, clientChangesApplied, connection); await this.InterceptAsync(databaseChangesAppliedArgs, cancellationToken).ConfigureAwait(false); this.ReportProgress(ctx, progress, databaseChangesAppliedArgs); } catch (Exception ex) { RaiseError(ex); } finally { if (connection != null && connection.State != ConnectionState.Closed) { connection.Close(); } } return(clientChangesApplied, scope); } }
ApplyChangesAsync(SyncContext context, MessageApplyChanges message, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress = null) { var changeApplicationAction = ChangeApplicationAction.Continue; var changesApplied = new DatabaseChangesApplied(); var hasChanges = await message.Changes.HasDataAsync(); // Check if we have some data available if (!hasChanges) { return(context, changesApplied); } context.SyncStage = SyncStage.DatabaseChangesApplying; // Launch any interceptor if available await this.InterceptAsync(new DatabaseChangesApplyingArgs(context, connection, transaction)).ConfigureAwait(false); // Disable check constraints // Because Sqlite does not support "PRAGMA foreign_keys=OFF" Inside a transaction // Report this disabling constraints brefore opening a transaction if (message.DisableConstraintsOnApplyChanges) { this.DisableConstraints(context, message.Schema, connection, transaction); } // ----------------------------------------------------- // 0) Check if we are in a reinit mode // ----------------------------------------------------- if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal) { changeApplicationAction = this.ResetInternal(context, message.Schema, connection, transaction); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new RollbackException("Rollback during reset tables"); } } // ----------------------------------------------------- // 1) Applying deletes. Do not apply deletes if we are in a new database // ----------------------------------------------------- if (!message.IsNew) { // for delete we must go from Up to Down foreach (var table in message.Schema.Tables.Reverse()) { changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection, transaction, DataRowState.Deleted, changesApplied, cancellationToken, progress).ConfigureAwait(false); } // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new RollbackException("Rollback during applying deletes"); } } // ----------------------------------------------------- // 2) Applying Inserts and Updates. Apply in table order // ----------------------------------------------------- foreach (var table in message.Schema.Tables) { changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection, transaction, DataRowState.Modified, changesApplied, cancellationToken, progress).ConfigureAwait(false); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new RollbackException("Rollback during applying updates"); } } // Progress & Interceptor context.SyncStage = SyncStage.DatabaseChangesApplied; var databaseChangesAppliedArgs = new DatabaseChangesAppliedArgs(context, changesApplied, connection, transaction); this.ReportProgress(context, progress, databaseChangesAppliedArgs, connection, transaction); await this.InterceptAsync(databaseChangesAppliedArgs).ConfigureAwait(false); // Re enable check constraints if (message.DisableConstraintsOnApplyChanges) { this.EnableConstraints(context, message.Schema, connection, transaction); } // clear the changes because we don't need them anymore message.Changes.Clear(message.CleanFolder); return(context, changesApplied); }