| | | 1 | | namespace ZhEaIsNsAaBn.DbSeeder.Design; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Compares a table's snapshot with its rows now and builds the migration's table file: |
| | | 5 | | /// new row → <c>Upsert</c>; changed row → <c>Upsert</c>; row gone → <c>Delete</c>; flag set since → <c>SoftDelete</c>; |
| | | 6 | | /// <c>Restore</c>. <b>Every key that existed in the snapshot and is touched gets its <c>Before</c> row</b> (data column |
| | | 7 | | /// </summary> |
| | | 8 | | internal static class SeedDiff |
| | | 9 | | { |
| | | 10 | | public static (DesignTable File, IReadOnlyList<string> Warnings) Compute( ExportedTable snapshot, ExportedTable curr |
| | | 11 | | { |
| | 210 | 12 | | var file = new DesignTable { Entity = current.Shape.EntityType.Name, MatchBy = current.Shape.IsBusinessKey ? |
| | 210 | 13 | | var then = snapshot.Rows.ToDictionary( r => r.Key, StringComparer.Ordinal ); |
| | 210 | 14 | | var now = current.Rows.Select( r => r.Key ).ToHashSet( StringComparer.Ordinal ); |
| | 210 | 15 | | var columns = current.Shape.DataColumns.Select( c => c.PropertyName ).ToList(); |
| | 210 | 16 | | var before = new HashSet<string>( StringComparer.Ordinal ); |
| | 210 | 17 | | var warnings = new List<string>(); |
| | | 18 | | |
| | | 19 | | |
| | 1560 | 20 | | foreach ( var row in current.Rows ) |
| | | 21 | | { |
| | 570 | 22 | | var existed = then.TryGetValue( row.Key, out var old ); |
| | | 23 | | |
| | 621 | 24 | | if ( !existed ) { file.Upsert.Add( row ); } |
| | 519 | 25 | | else if ( Changed( old!, row, columns ) is { Count: > 0 } changed ) |
| | | 26 | | { |
| | 3 | 27 | | file.Upsert.Add( row ); |
| | 3 | 28 | | before.Add( row.Key ); |
| | 3 | 29 | | warnings.AddRange( Repurposed( current, row, old!, changed ) ); |
| | | 30 | | } |
| | | 31 | | |
| | | 32 | | |
| | 570 | 33 | | var deletedNow = current.SoftDeleted.Contains( row.Key ); |
| | 570 | 34 | | var deletedThen = existed && snapshot.SoftDeleted.Contains( row.Key ); |
| | | 35 | | |
| | 570 | 36 | | if ( deletedNow && !deletedThen ) |
| | | 37 | | { |
| | 3 | 38 | | file.SoftDelete.Add( row.Only( current.MatchNames ) ); |
| | | 39 | | |
| | 6 | 40 | | if ( existed ) { before.Add( row.Key ); } |
| | | 41 | | } |
| | 567 | 42 | | else if ( !deletedNow && deletedThen ) |
| | | 43 | | { |
| | 3 | 44 | | file.Restore.Add( row.Only( current.MatchNames ) ); |
| | 3 | 45 | | before.Add( row.Key ); |
| | | 46 | | } |
| | | 47 | | } |
| | | 48 | | |
| | 426 | 49 | | foreach ( var old in snapshot.Rows.Where( r => !now.Contains( r.Key ) ) ) |
| | | 50 | | { |
| | 3 | 51 | | file.Delete.Add( old.Only( current.MatchNames ) ); |
| | 3 | 52 | | before.Add( old.Key ); |
| | | 53 | | } |
| | | 54 | | |
| | | 55 | | |
| | | 56 | | // Only properties the model still has, so the runtime accepts the Before rows. |
| | 210 | 57 | | file.Before.AddRange( before.Select( k => then[ k ].Only( columns ) ) ); |
| | | 58 | | |
| | 210 | 59 | | return ( file, warnings ); |
| | | 60 | | } |
| | | 61 | | |
| | | 62 | | |
| | | 63 | | private static List<string> Changed( DesignRow old, DesignRow now, IReadOnlyList<string> columns ) |
| | 519 | 64 | | => columns.Where( c => ( old[ c ] ?? "null" ) != ( now[ c ] ?? "null" ) ).ToList(); |
| | | 65 | | |
| | | 66 | | /// §8.4: same key, but a name-like column changed — the row may have been reused for something else. |
| | | 67 | | private static IEnumerable<string> Repurposed( ExportedTable table, DesignRow now, DesignRow old, IReadOnlyList<stri |
| | 3 | 68 | | => changed.Where( c => c.StartsWith( "Name", StringComparison.Ordinal ) || c.StartsWith( "Title", StringComparis |
| | 3 | 69 | | .Select( c => $"{table.Shape.EntityType.ClrType.Name} {now.Key.Replace( SeedKeyComparer.Separator, ',' |
| | | 70 | | } |