| | | 1 | | #region Usings |
| | | 2 | | using System.Data; |
| | | 3 | | using System.Diagnostics; |
| | | 4 | | #endregion |
| | | 5 | | |
| | | 6 | | namespace ZhEaIsNsAaBn.DbSeeder; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Rolling seed migrations back (§6.3), newest first, from their <c>Before</c> images. All selected migrations run in < |
| | | 10 | | /// transaction: a blocker anywhere (rows still referenced) means nothing changes. A plan is the same run, always rolled |
| | | 11 | | /// <list type="number"> |
| | | 12 | | /// <item>Per table, principals first: rows the migration changed or deleted get their <c>Before</c> values back (update |
| | | 13 | | /// again); rows it deleted or soft-deleted lose the flag.</item> |
| | | 14 | | /// <item>Per table, dependents first: rows it inserted are removed (delete mode: run override → entity setting → <c>Aut |
| | | 15 | | /// delete is FK-checked); rows it restored are soft-deleted again.</item> |
| | | 16 | | /// <item>The history row is removed; an audit row (<c>Down</c>, or <c>SoftDown</c> when inserted rows were soft-deleted |
| | | 17 | | /// </list> |
| | | 18 | | /// </summary> |
| | | 19 | | internal static class SeedRollback |
| | | 20 | | { |
| | | 21 | | private const string Down = nameof( Down ); |
| | | 22 | | private const string SoftDown = nameof( SoftDown ); |
| | | 23 | | private const string ForceDown = nameof( ForceDown ); |
| | | 24 | | |
| | | 25 | | |
| | | 26 | | /// Rolls back <paramref name="steps"/> migrations, or every migration newer than <paramref name="targetMigrationId" |
| | | 27 | | public static async Task<LayerResponse<SeedRunResult>> DownAsync( |
| | | 28 | | DbContext dbContext, |
| | | 29 | | SeedConfiguration configuration, |
| | | 30 | | ISeedDatabaseProvider provider, |
| | | 31 | | int? steps, |
| | | 32 | | string? targetMigrationId, |
| | | 33 | | SeedDeleteMode? deleteMode, |
| | | 34 | | bool async, |
| | | 35 | | CancellationToken cancellationToken, |
| | | 36 | | bool force = false |
| | | 37 | | ) |
| | | 38 | | { |
| | 52 | 39 | | var run = await RunAsync( dbContext, configuration, provider, steps, targetMigrationId, deleteMode, dryRun: fals |
| | | 40 | | |
| | 49 | 41 | | return run.IsFailure ? LayerResponse<SeedRunResult>.Failure( run ) : new SeedRunResult { Ids = run.Result!.Migra |
| | 49 | 42 | | } |
| | | 43 | | |
| | | 44 | | /// What <see cref="DownAsync"/> would do (a dry run, rolled back). |
| | | 45 | | public static Task<LayerResponse<SeedRollbackPlan>> PlanAsync( |
| | | 46 | | DbContext dbContext, |
| | | 47 | | SeedConfiguration configuration, |
| | | 48 | | ISeedDatabaseProvider provider, |
| | | 49 | | int? steps, |
| | | 50 | | string? targetMigrationId, |
| | | 51 | | SeedDeleteMode? deleteMode, |
| | | 52 | | bool async, |
| | | 53 | | CancellationToken cancellationToken, |
| | | 54 | | bool force = false |
| | | 55 | | ) |
| | 65 | 56 | | => RunAsync( dbContext, configuration, provider, steps, targetMigrationId, deleteMode, dryRun: true, force, asyn |
| | | 57 | | |
| | | 58 | | |
| | | 59 | | private static Task<LayerResponse<SeedRollbackPlan>> RunAsync( |
| | | 60 | | DbContext dbContext, |
| | | 61 | | SeedConfiguration configuration, |
| | | 62 | | ISeedDatabaseProvider provider, |
| | | 63 | | int? steps, |
| | | 64 | | string? targetMigrationId, |
| | | 65 | | SeedDeleteMode? deleteMode, |
| | | 66 | | bool dryRun, |
| | | 67 | | bool force, |
| | | 68 | | bool async, |
| | | 69 | | CancellationToken cancellationToken |
| | | 70 | | ) |
| | | 71 | | { |
| | 117 | 72 | | if ( dbContext.Database.CurrentTransaction is not null ) |
| | | 73 | | { |
| | 3 | 74 | | throw new InvalidOperationException( "Seed rollbacks run in their own transaction: call them with no transac |
| | | 75 | | } |
| | | 76 | | |
| | 114 | 77 | | if ( configuration.Migrations.IsFailure ) { return Task.FromResult( LayerResponse<SeedRollbackPlan>.Failure( con |
| | | 78 | | |
| | | 79 | | |
| | 114 | 80 | | return SeedEngine.SessionAsync( dbContext, |
| | 114 | 81 | | configuration, |
| | 114 | 82 | | provider, |
| | 114 | 83 | | takeLock: true, |
| | 114 | 84 | | () => BodyAsync( dbContext, configuration, provider, steps, targetMigrationId, d |
| | 114 | 85 | | async, |
| | 114 | 86 | | cancellationToken ); |
| | | 87 | | } |
| | | 88 | | |
| | | 89 | | private static async Task<LayerResponse<SeedRollbackPlan>> BodyAsync( |
| | | 90 | | DbContext dbContext, |
| | | 91 | | SeedConfiguration configuration, |
| | | 92 | | ISeedDatabaseProvider provider, |
| | | 93 | | int? steps, |
| | | 94 | | string? targetMigrationId, |
| | | 95 | | SeedDeleteMode? deleteMode, |
| | | 96 | | bool dryRun, |
| | | 97 | | bool force, |
| | | 98 | | bool async, |
| | | 99 | | CancellationToken cancellationToken |
| | | 100 | | ) |
| | | 101 | | { |
| | 114 | 102 | | var embedded = configuration.Migrations.Result!.ToDictionary( m => m.Id, StringComparer.Ordinal ); |
| | 114 | 103 | | var history = await provider.ReadHistoryAsync( dbContext, configuration.ContextKey, async, cancellationToken ). |
| | 114 | 104 | | var applied = history.OrderByDescending( h => h.MigrationId, StringComparer.Ordinal ).ToList(); |
| | | 105 | | |
| | | 106 | | |
| | 114 | 107 | | if ( targetMigrationId is not null && !embedded.ContainsKey( targetMigrationId ) && applied.All( h => h.Migratio |
| | | 108 | | { |
| | 4 | 109 | | return SeedErrors.TargetNotFound( targetMigrationId ); |
| | | 110 | | } |
| | | 111 | | |
| | 110 | 112 | | var selected = targetMigrationId is null |
| | 110 | 113 | | ? applied.Take( steps ?? 0 ).ToList() |
| | 110 | 114 | | : applied.Where( h => string.CompareOrdinal( h.MigrationId, targetMigrationId ) > 0 ).ToList( |
| | | 115 | | |
| | | 116 | | |
| | | 117 | | // Bind everything before touching data: a missing migration or a file that can't be rolled back stops here. |
| | 110 | 118 | | var migrations = new List<(SeedMigrationFiles Files, IReadOnlyList<SeedBoundTable> Tables)>(); |
| | 110 | 119 | | var warnings = new List<Error>(); |
| | | 120 | | |
| | 433 | 121 | | foreach ( var row in selected ) |
| | | 122 | | { |
| | 114 | 123 | | if ( !embedded.TryGetValue( row.MigrationId, out var files ) ) { return SeedErrors.MigrationNotFound( row.Mi |
| | | 124 | | |
| | 106 | 125 | | if ( files.Checksum != row.Checksum ) { warnings.Add( SeedErrors.ChecksumMismatch( files.Id ) ); } |
| | | 126 | | |
| | | 127 | | |
| | 106 | 128 | | var bound = SeedMigrationBinder.Bind( dbContext.Model, configuration, files ); |
| | | 129 | | |
| | 106 | 130 | | if ( bound.IsFailure ) { return LayerResponse<SeedRollbackPlan>.Failure( bound ); } |
| | | 131 | | |
| | 109 | 132 | | if ( MissingBefore( bound.Result! ) is { } missing ) { return missing; } |
| | | 133 | | |
| | 103 | 134 | | migrations.Add( ( files, bound.Result! ) ); |
| | | 135 | | } |
| | | 136 | | |
| | | 137 | | |
| | 103 | 138 | | var run = new RunState(); |
| | | 139 | | |
| | 103 | 140 | | if ( migrations.Count > 0 ) |
| | | 141 | | { |
| | | 142 | | try |
| | | 143 | | { |
| | 103 | 144 | | var strategy = dbContext.Database.CreateExecutionStrategy(); |
| | 103 | 145 | | var result = async |
| | 103 | 146 | | ? await strategy.ExecuteAsync( token => InTransactionAsync( true, token ), cancellation |
| | 103 | 147 | | : strategy.Execute( () => SyncPath.Result( InTransactionAsync( false, cancellationToken |
| | | 148 | | |
| | 104 | 149 | | if ( result.IsFailure ) { return await FailedAsync( result.Errors ).ConfigureAwait( false ); } |
| | 96 | 150 | | } |
| | 0 | 151 | | catch ( OperationCanceledException ) { throw; } |
| | 6 | 152 | | catch ( Exception ex ) { return await FailedAsync( [ provider.Classify( ex ) ?? Error.Unknown( ex ) ] ).Conf |
| | | 153 | | |
| | 96 | 154 | | if ( !dryRun && run.Blockers.Count > 0 ) |
| | | 155 | | { |
| | 4 | 156 | | return await FailedAsync( [ SeedErrors.RowsReferenced( run.BlockedTable!, run.Blockers ) ] ).ConfigureAw |
| | | 157 | | } |
| | | 158 | | } |
| | | 159 | | |
| | | 160 | | |
| | 92 | 161 | | return new SeedRollbackPlan |
| | 92 | 162 | | { |
| | 92 | 163 | | Migrations = migrations.Select( m => m.Files.Id ).ToList(), |
| | 92 | 164 | | Steps = run.Steps, |
| | 92 | 165 | | Blockers = run.Blockers, |
| | 92 | 166 | | Warnings = warnings, |
| | 92 | 167 | | ForceTree = run.ForceTree, |
| | 92 | 168 | | ForceRefusal = run.ForceRefusal |
| | 92 | 169 | | }; |
| | | 170 | | |
| | | 171 | | |
| | | 172 | | async Task<LayerResponse> InTransactionAsync( bool runAsync, CancellationToken token ) |
| | | 173 | | { |
| | | 174 | | run.Reset(); |
| | | 175 | | |
| | | 176 | | var transaction = runAsync |
| | | 177 | | ? await dbContext.Database.BeginTransactionAsync( IsolationLevel.ReadCommitted, token |
| | | 178 | | : dbContext.Database.BeginTransaction( IsolationLevel.ReadCommitted ); |
| | | 179 | | |
| | | 180 | | try |
| | | 181 | | { |
| | | 182 | | foreach ( var (files, tables) in migrations ) |
| | | 183 | | { |
| | | 184 | | run.Current = files.Id; |
| | | 185 | | |
| | | 186 | | var one = await RollBackOneAsync( dbContext, configuration, provider, files, tables, deleteMode, run |
| | | 187 | | |
| | | 188 | | if ( one.IsFailure ) { return one; } |
| | | 189 | | } |
| | | 190 | | |
| | | 191 | | |
| | | 192 | | // A plan and a blocked rollback change nothing: disposing the transaction rolls it back. |
| | | 193 | | if ( dryRun || run.Blockers.Count > 0 ) { return LayerResponse.Success(); } |
| | | 194 | | |
| | | 195 | | if ( runAsync ) { await transaction.CommitAsync( token ).ConfigureAwait( false ); } |
| | | 196 | | else { transaction.Commit(); } |
| | | 197 | | |
| | | 198 | | return LayerResponse.Success(); |
| | | 199 | | } |
| | | 200 | | finally |
| | | 201 | | { |
| | | 202 | | if ( runAsync ) { await transaction.DisposeAsync().ConfigureAwait( false ); } |
| | | 203 | | else { transaction.Dispose(); } |
| | | 204 | | } |
| | | 205 | | } |
| | | 206 | | |
| | | 207 | | // The transaction was rolled back; a real rollback records the failure of the migration it stopped at. |
| | | 208 | | async Task<LayerResponse<SeedRollbackPlan>> FailedAsync( List<Error> errors ) |
| | | 209 | | { |
| | | 210 | | if ( dryRun || ( run.BlockedMigration ?? run.Current ) is not { } failed ) { return errors; } |
| | | 211 | | |
| | | 212 | | |
| | | 213 | | var audit = await SeedEngine.TryAsync( provider, |
| | | 214 | | () => provider.WriteAuditAsync( dbContext, Audit( configuration, fail |
| | | 215 | | async, cancellationToken ) ) |
| | | 216 | | .ConfigureAwait( false ); |
| | | 217 | | |
| | | 218 | | if ( audit is not null ) { errors.Add( audit ); } |
| | | 219 | | |
| | | 220 | | return errors; |
| | | 221 | | } |
| | 114 | 222 | | } |
| | | 223 | | |
| | | 224 | | /// One migration, newest first. Blockers are collected (no delete for them) so a plan lists them all. |
| | | 225 | | private static async Task<LayerResponse> RollBackOneAsync( |
| | | 226 | | DbContext dbContext, |
| | | 227 | | SeedConfiguration configuration, |
| | | 228 | | ISeedDatabaseProvider provider, |
| | | 229 | | SeedMigrationFiles files, |
| | | 230 | | IReadOnlyList<SeedBoundTable> tables, |
| | | 231 | | SeedDeleteMode? deleteMode, |
| | | 232 | | RunState run, |
| | | 233 | | bool dryRun, |
| | | 234 | | bool force, |
| | | 235 | | bool async, |
| | | 236 | | CancellationToken cancellationToken |
| | | 237 | | ) |
| | | 238 | | { |
| | 103 | 239 | | using var activity = DbSeederDiagnostics.ActivitySource.StartActivity( "Roll back seed migration" ); |
| | 103 | 240 | | activity?.SetTag( "MigrationId", files.Id ); |
| | | 241 | | |
| | 103 | 242 | | var counts = new MigrationCounts(); |
| | 103 | 243 | | var writer = new SeedRowWriter( configuration.Options, SeedEngine.Now( configuration ) ); |
| | 103 | 244 | | var startedOn = SeedEngine.Now( configuration ); |
| | 103 | 245 | | var usedSoft = false; |
| | 103 | 246 | | var forced = new List<SeedForceDeleted>(); |
| | | 247 | | |
| | 103 | 248 | | run.StartedOn = startedOn; |
| | | 249 | | |
| | | 250 | | |
| | | 251 | | // 1. Put back changed / deleted rows, principals first; then clear the flag of rows the migration (soft-)delete |
| | 526 | 252 | | foreach ( var table in tables ) |
| | | 253 | | { |
| | 160 | 254 | | var touched = table.KeysOf( table.UpsertColumns, table.UpsertRows ).Concat( table.KeysOf( table.DeleteKeys ) |
| | 160 | 255 | | var putBack = table.BeforeRows.Zip( table.KeysOf( table.BeforeColumns, table.BeforeRows ) ).Where( b => touc |
| | | 256 | | |
| | 160 | 257 | | if ( putBack.Count > 0 ) |
| | | 258 | | { |
| | 78 | 259 | | var writes = writer.Upserts( table, table.BeforeColumns, putBack, files.Id ); |
| | | 260 | | |
| | 78 | 261 | | if ( writes.IsFailure ) { return writes; } |
| | | 262 | | |
| | | 263 | | |
| | 78 | 264 | | var rows = 0; |
| | | 265 | | |
| | 312 | 266 | | foreach ( var write in writes.Result! ) |
| | | 267 | | { |
| | 78 | 268 | | var written = await provider.UpsertAsync( dbContext, write, async, cancellationToken ).ConfigureAwai |
| | | 269 | | |
| | 78 | 270 | | counts.Inserted += written.Inserted; |
| | 78 | 271 | | counts.Updated += written.Updated; |
| | 78 | 272 | | rows += written.Inserted + written.Updated; |
| | | 273 | | } |
| | | 274 | | |
| | 78 | 275 | | run.Steps.Add( new SeedRollbackStep( files.Id, table.Table.DisplayName, SeedRollbackOperation.PutBack, r |
| | | 276 | | } |
| | | 277 | | |
| | | 278 | | |
| | 160 | 279 | | var undelete = table.DeleteKeys.Concat( table.SoftDeleteKeys ).ToList(); |
| | | 280 | | |
| | 160 | 281 | | if ( undelete.Count > 0 && table.Table.SoftDeleteFlag is not null ) |
| | | 282 | | { |
| | 27 | 283 | | var values = writer.RestoreValues( table ); |
| | | 284 | | |
| | 27 | 285 | | if ( values.IsFailure ) { return values; } |
| | | 286 | | |
| | | 287 | | |
| | 27 | 288 | | var restored = await provider.RestoreAsync( dbContext, Keys( table, undelete ), values.Result!, async, c |
| | | 289 | | |
| | 27 | 290 | | counts.Updated += restored; |
| | 27 | 291 | | run.Steps.Add( new SeedRollbackStep( files.Id, table.Table.DisplayName, SeedRollbackOperation.Undelete, |
| | | 292 | | } |
| | 160 | 293 | | } |
| | | 294 | | |
| | | 295 | | |
| | | 296 | | // 2. Remove inserted rows and soft-delete restored rows, dependents first. |
| | 519 | 297 | | foreach ( var table in tables.Reverse() ) |
| | | 298 | | { |
| | 160 | 299 | | var before = table.KeysOf( table.BeforeColumns, table.BeforeRows ).ToHashSet( StringComparer.Ordinal ); |
| | 160 | 300 | | var inserted = table.UpsertRows.Zip( table.KeysOf( table.UpsertColumns, table.UpsertRows ) ).Where( r => !be |
| | | 301 | | |
| | 160 | 302 | | if ( inserted.Count > 0 ) |
| | | 303 | | { |
| | 126 | 304 | | var mode = deleteMode ?? table.Roles.DeleteMode; |
| | 126 | 305 | | var soft = mode == SeedDeleteMode.Soft || ( mode == SeedDeleteMode.Auto && table.Table.SoftDeleteFlag is |
| | | 306 | | |
| | 126 | 307 | | if ( soft ) |
| | | 308 | | { |
| | 43 | 309 | | var removed = await SoftDeleteAsync( table, inserted ).ConfigureAwait( false ); |
| | | 310 | | |
| | 47 | 311 | | if ( removed.IsFailure ) { return removed; } |
| | | 312 | | |
| | 39 | 313 | | usedSoft = true; |
| | | 314 | | } |
| | | 315 | | else |
| | | 316 | | { |
| | 83 | 317 | | var keys = Keys( table, inserted ); |
| | 83 | 318 | | var blockers = await provider.CountReferencesAsync( dbContext, keys, async, cancellationToken ).Conf |
| | | 319 | | |
| | 83 | 320 | | if ( blockers.Count > 0 && force && !dryRun ) |
| | | 321 | | { |
| | | 322 | | // Design-time force: the dependent rows go first, bottom-up, in this same transaction. |
| | 6 | 323 | | forced.AddRange( await provider.ForceDeleteAsync( dbContext, keys, async, cancellationToken ).Co |
| | 6 | 324 | | blockers = []; |
| | | 325 | | } |
| | | 326 | | |
| | 83 | 327 | | if ( blockers.Count > 0 ) |
| | | 328 | | { |
| | 47 | 329 | | run.Block( table.Table.DisplayName, blockers ); |
| | 47 | 330 | | run.Steps.Add( new SeedRollbackStep( files.Id, table.Table.DisplayName, SeedRollbackOperation.De |
| | | 331 | | |
| | 47 | 332 | | if ( force ) |
| | | 333 | | { |
| | 30 | 334 | | var tree = await provider.PlanForceDeleteAsync( dbContext, keys, async, cancellationToken ). |
| | | 335 | | |
| | 30 | 336 | | run.ForceTree.AddRange( tree.Nodes ); |
| | 30 | 337 | | run.ForceRefusal ??= tree.Refusal; |
| | | 338 | | } |
| | | 339 | | } |
| | | 340 | | else |
| | | 341 | | { |
| | 36 | 342 | | var deleted = await provider.DeleteAsync( dbContext, keys, async, cancellationToken ).ConfigureA |
| | | 343 | | |
| | 33 | 344 | | counts.Deleted += deleted; |
| | 33 | 345 | | run.Steps.Add( new SeedRollbackStep( files.Id, table.Table.DisplayName, SeedRollbackOperation.De |
| | | 346 | | } |
| | 80 | 347 | | } |
| | | 348 | | } |
| | | 349 | | |
| | | 350 | | |
| | 153 | 351 | | if ( table.RestoreKeys.Count > 0 ) |
| | | 352 | | { |
| | 7 | 353 | | var again = await SoftDeleteAsync( table, table.RestoreKeys ).ConfigureAwait( false ); |
| | | 354 | | |
| | 7 | 355 | | if ( again.IsFailure ) { return again; } |
| | | 356 | | } |
| | 153 | 357 | | } |
| | | 358 | | |
| | | 359 | | |
| | 96 | 360 | | await provider.DeleteHistoryAsync( dbContext, files.Id, configuration.ContextKey, async, cancellationToken ).Con |
| | | 361 | | |
| | 96 | 362 | | if ( !dryRun && run.Blockers.Count == 0 ) |
| | | 363 | | { |
| | 27 | 364 | | var audit = Audit( configuration, files.Id, forced.Count > 0 ? ForceDown : usedSoft ? SoftDown : Down, start |
| | | 365 | | |
| | 27 | 366 | | await provider.WriteAuditAsync( dbContext, forced.Count > 0 ? WithDetails( audit, forced ) : audit, async, c |
| | | 367 | | } |
| | | 368 | | |
| | | 369 | | |
| | 96 | 370 | | activity?.SetTag( "RowsInserted", counts.Inserted ); |
| | 96 | 371 | | activity?.SetTag( "RowsUpdated", counts.Updated ); |
| | 96 | 372 | | activity?.SetTag( "RowsDeleted", counts.Deleted ); |
| | 96 | 373 | | activity?.SetStatus( ActivityStatusCode.Ok ); |
| | | 374 | | |
| | 96 | 375 | | return LayerResponse.Success(); |
| | | 376 | | |
| | | 377 | | |
| | | 378 | | async Task<LayerResponse> SoftDeleteAsync( SeedBoundTable table, IReadOnlyList<IReadOnlyList<object?>> keyRows ) |
| | | 379 | | { |
| | | 380 | | if ( table.Table.SoftDeleteFlag is null ) { return SeedErrors.SoftDeleteNotSupported( table.EntityType.Name |
| | | 381 | | |
| | | 382 | | |
| | | 383 | | var values = writer.SoftDeleteValues( table ); |
| | | 384 | | |
| | | 385 | | if ( values.IsFailure ) { return values; } |
| | | 386 | | |
| | | 387 | | |
| | | 388 | | var removed = await provider.SoftDeleteAsync( dbContext, Keys( table, keyRows ), values.Result!, async, canc |
| | | 389 | | |
| | | 390 | | counts.Deleted += removed; |
| | | 391 | | run.Steps.Add( new SeedRollbackStep( files.Id, table.Table.DisplayName, SeedRollbackOperation.SoftDelete, re |
| | | 392 | | |
| | | 393 | | return LayerResponse.Success(); |
| | | 394 | | } |
| | 100 | 395 | | } |
| | | 396 | | |
| | | 397 | | /// A <c>Delete</c> key with no <c>Before</c> row can't be put back. |
| | | 398 | | internal static Error? MissingBefore( IReadOnlyList<SeedBoundTable> tables ) |
| | | 399 | | { |
| | 350 | 400 | | foreach ( var table in tables.Where( t => t.DeleteKeys.Count > 0 ) ) |
| | | 401 | | { |
| | 57 | 402 | | var before = table.KeysOf( table.BeforeColumns, table.BeforeRows ).ToHashSet( StringComparer.Ordinal ); |
| | | 403 | | |
| | 57 | 404 | | if ( table.KeysOf( table.DeleteKeys ).Any( k => !before.Contains( k ) ) ) |
| | | 405 | | { |
| | 6 | 406 | | return SeedErrors.InvalidDataFile( table.File, "A Delete row has no Before row, so the migration can't b |
| | | 407 | | } |
| | | 408 | | } |
| | | 409 | | |
| | 115 | 410 | | return null; |
| | 6 | 411 | | } |
| | | 412 | | |
| | 156 | 413 | | private static SeedTableKeys Keys( SeedBoundTable table, IReadOnlyList<IReadOnlyList<object?>> keyRows ) => new() { |
| | | 414 | | |
| | | 415 | | /// The match-key values of a full upsert row. |
| | | 416 | | private static List<object?> MatchValues( SeedBoundTable table, IReadOnlyList<object?> row ) |
| | 126 | 417 | | => table.Table.MatchColumns.Select( m => row[ table.UpsertColumns.Select( c => c.ColumnName ).ToList().IndexOf( |
| | | 418 | | |
| | | 419 | | private static SeedAuditRow Audit( SeedConfiguration configuration, string migrationId, string operation, DateTime s |
| | 38 | 420 | | => new() |
| | 38 | 421 | | { |
| | 38 | 422 | | MigrationId = migrationId, |
| | 38 | 423 | | ContextKey = configuration.ContextKey, |
| | 38 | 424 | | Operation = operation, |
| | 38 | 425 | | StartedOn = startedOn, |
| | 38 | 426 | | FinishedOn = SeedEngine.Now( configuration ), |
| | 38 | 427 | | Succeeded = errorCode is null, |
| | 38 | 428 | | ErrorCode = errorCode, |
| | 38 | 429 | | RowsInserted = counts.Inserted, |
| | 38 | 430 | | RowsUpdated = counts.Updated, |
| | 38 | 431 | | RowsDeleted = counts.Deleted, |
| | 38 | 432 | | OsUser = Environment.UserName, |
| | 38 | 433 | | MachineName = Environment.MachineName, |
| | 38 | 434 | | ToolVersion = DbSeederDiagnostics.Version |
| | 38 | 435 | | }; |
| | | 436 | | |
| | | 437 | | |
| | | 438 | | /// The audit row of a forced rollback carries the deleted dependent rows (JSON; may hold personal data). |
| | | 439 | | private static SeedAuditRow WithDetails( SeedAuditRow audit, IReadOnlyList<SeedForceDeleted> forced ) |
| | | 440 | | { |
| | 3 | 441 | | using var stream = new MemoryStream(); |
| | | 442 | | |
| | 3 | 443 | | using ( var writer = new Utf8JsonWriter( stream ) ) |
| | | 444 | | { |
| | 3 | 445 | | writer.WriteStartObject(); |
| | 3 | 446 | | writer.WriteStartArray( "Tables" ); |
| | | 447 | | |
| | 18 | 448 | | foreach ( var table in forced ) |
| | | 449 | | { |
| | 6 | 450 | | writer.WriteStartObject(); |
| | 6 | 451 | | writer.WriteString( "Table", table.Table ); |
| | 6 | 452 | | writer.WriteNumber( "Rows", table.Rows ); |
| | 6 | 453 | | writer.WritePropertyName( "Deleted" ); |
| | 6 | 454 | | writer.WriteRawValue( table.RowsJson ); |
| | 6 | 455 | | writer.WriteEndObject(); |
| | | 456 | | } |
| | | 457 | | |
| | 3 | 458 | | writer.WriteEndArray(); |
| | 3 | 459 | | writer.WriteEndObject(); |
| | 3 | 460 | | } |
| | | 461 | | |
| | | 462 | | |
| | 3 | 463 | | return new SeedAuditRow |
| | 3 | 464 | | { |
| | 3 | 465 | | MigrationId = audit.MigrationId, |
| | 3 | 466 | | ContextKey = audit.ContextKey, |
| | 3 | 467 | | Operation = audit.Operation, |
| | 3 | 468 | | StartedOn = audit.StartedOn, |
| | 3 | 469 | | FinishedOn = audit.FinishedOn, |
| | 3 | 470 | | Succeeded = audit.Succeeded, |
| | 3 | 471 | | ErrorCode = audit.ErrorCode, |
| | 3 | 472 | | RowsInserted = audit.RowsInserted, |
| | 3 | 473 | | RowsUpdated = audit.RowsUpdated, |
| | 3 | 474 | | RowsDeleted = audit.RowsDeleted + forced.Sum( f => f.Rows ), |
| | 3 | 475 | | OsUser = audit.OsUser, |
| | 3 | 476 | | MachineName = audit.MachineName, |
| | 3 | 477 | | ToolVersion = audit.ToolVersion, |
| | 3 | 478 | | Details = System.Text.Encoding.UTF8.GetString( stream.ToArray() ) |
| | 3 | 479 | | }; |
| | 3 | 480 | | } |
| | | 481 | | |
| | | 482 | | |
| | | 483 | | private sealed class MigrationCounts |
| | | 484 | | { |
| | | 485 | | public int Inserted { get; set; } |
| | | 486 | | |
| | | 487 | | public int Updated { get; set; } |
| | | 488 | | |
| | | 489 | | public int Deleted { get; set; } |
| | | 490 | | } |
| | | 491 | | |
| | | 492 | | /// Steps and blockers of the whole run (restart with a retried transaction). |
| | | 493 | | private sealed class RunState |
| | | 494 | | { |
| | | 495 | | public List<SeedRollbackStep> Steps { get; } = []; |
| | | 496 | | |
| | | 497 | | public List<SeedBlocker> Blockers { get; } = []; |
| | | 498 | | |
| | | 499 | | public List<SeedForceNode> ForceTree { get; } = []; |
| | | 500 | | |
| | | 501 | | public string? ForceRefusal { get; set; } |
| | | 502 | | |
| | | 503 | | public string? BlockedTable { get; private set; } |
| | | 504 | | |
| | | 505 | | public string? BlockedMigration { get; private set; } |
| | | 506 | | |
| | | 507 | | public string? Current { get; set; } |
| | | 508 | | |
| | | 509 | | public DateTime StartedOn { get; set; } |
| | | 510 | | |
| | | 511 | | |
| | | 512 | | public void Block( string table, IEnumerable<SeedBlocker> blockers ) |
| | | 513 | | { |
| | 47 | 514 | | BlockedTable ??= table; |
| | 47 | 515 | | BlockedMigration ??= Current; |
| | 47 | 516 | | Blockers.AddRange( blockers ); |
| | 47 | 517 | | } |
| | | 518 | | |
| | | 519 | | public void Reset() |
| | | 520 | | { |
| | 103 | 521 | | Steps.Clear(); |
| | 103 | 522 | | Blockers.Clear(); |
| | 103 | 523 | | ForceTree.Clear(); |
| | 103 | 524 | | ForceRefusal = null; |
| | 103 | 525 | | BlockedTable = null; |
| | 103 | 526 | | BlockedMigration = null; |
| | 103 | 527 | | Current = null; |
| | 103 | 528 | | } |
| | | 529 | | } |
| | | 530 | | } |