| | | 1 | | namespace ZhEaIsNsAaBn.DbSeeder; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Splits the rows of a self-referencing table into dependency levels: level 0 references no row of the file (or only i |
| | | 5 | | /// only rows of earlier levels. Each level is one ordinary upsert, so a row is always written after the row it points t |
| | | 6 | | /// insert / update counts and audit stamps stay exact. |
| | | 7 | | /// </summary> |
| | | 8 | | internal static class SeedRowLevels |
| | | 9 | | { |
| | | 10 | | /// <param name="columns">The written columns.</param> |
| | | 11 | | /// <param name="rows">The rows (values aligned with <paramref name="columns"/>).</param> |
| | | 12 | | /// <param name="selfReferences">The table's foreign keys to itself whose columns are all written.</param> |
| | | 13 | | /// <returns>The levels; or the indexes of the rows left in a cycle.</returns> |
| | | 14 | | public static (IReadOnlyList<IReadOnlyList<IReadOnlyList<object?>>>? Levels, IReadOnlyList<int> Cycle) Split( |
| | | 15 | | IReadOnlyList<SeedColumn> columns, |
| | | 16 | | IReadOnlyList<IReadOnlyList<object?>> rows, |
| | | 17 | | IReadOnlyList<IForeignKey> selfReferences |
| | | 18 | | ) |
| | | 19 | | { |
| | 1856 | 20 | | if ( selfReferences.Count == 0 ) { return ( [ rows ], [] ); } |
| | | 21 | | |
| | | 22 | | |
| | 178 | 23 | | var index = columns.Select( ( c, i ) => (c.PropertyName, i) ).ToDictionary( x => x.PropertyName, x => x.i |
| | 178 | 24 | | var dependencies = new HashSet<int>[ rows.Count ]; |
| | | 25 | | |
| | 712 | 26 | | foreach ( var foreignKey in selfReferences ) |
| | | 27 | | { |
| | 178 | 28 | | var principal = foreignKey.PrincipalKey.Properties.Select( p => index[ p.Name ] ).ToArray(); |
| | 178 | 29 | | var dependent = foreignKey.Properties.Select( p => index[ p.Name ] ).ToArray(); |
| | 178 | 30 | | var byKey = new Dictionary<string, int>( StringComparer.Ordinal ); |
| | | 31 | | |
| | 1949 | 32 | | for ( var r = 0; r < rows.Count; r++ ) { byKey.TryAdd( Key( rows[ r ], principal ), r ); } |
| | | 33 | | |
| | | 34 | | |
| | 1418 | 35 | | for ( var r = 0; r < rows.Count; r++ ) |
| | | 36 | | { |
| | 531 | 37 | | dependencies[ r ] ??= []; |
| | | 38 | | |
| | 531 | 39 | | if ( dependent.Any( c => rows[ r ][ c ] is null ) ) { continue; } |
| | | 40 | | |
| | 709 | 41 | | if ( byKey.TryGetValue( Key( rows[ r ], dependent ), out var parent ) && parent != r ) { dependencies[ r |
| | | 42 | | } |
| | | 43 | | } |
| | | 44 | | |
| | | 45 | | |
| | 178 | 46 | | var levels = new List<IReadOnlyList<IReadOnlyList<object?>>>(); |
| | 178 | 47 | | var done = new HashSet<int>(); |
| | | 48 | | |
| | 697 | 49 | | while ( done.Count < rows.Count ) |
| | | 50 | | { |
| | 522 | 51 | | var level = Enumerable.Range( 0, rows.Count ).Where( r => !done.Contains( r ) && dependencies[ r ].All( done |
| | | 52 | | |
| | 525 | 53 | | if ( level.Count == 0 ) { return ( null, Enumerable.Range( 0, rows.Count ).Where( r => !done.Contains( r ) ) |
| | | 54 | | |
| | | 55 | | |
| | 519 | 56 | | levels.Add( level.Select( r => rows[ r ] ).ToList() ); |
| | 519 | 57 | | done.UnionWith( level ); |
| | | 58 | | } |
| | | 59 | | |
| | | 60 | | |
| | 175 | 61 | | return ( levels, [] ); |
| | | 62 | | } |
| | | 63 | | |
| | | 64 | | |
| | | 65 | | /// The text of the values at <paramref name="columns"/> (a row key). |
| | 1627 | 66 | | internal static string Key( IReadOnlyList<object?> row, int[] columns ) => string.Join( '\u001F', columns.Select( c |
| | | 67 | | |
| | | 68 | | private static string Format( object? value ) |
| | 1627 | 69 | | => value switch |
| | 1627 | 70 | | { |
| | 0 | 71 | | null => "\u0000", |
| | 0 | 72 | | byte[] bytes => Convert.ToBase64String( bytes ), |
| | 0 | 73 | | DateTime date => date.ToString( "O", CultureInfo.InvariantCulture ), |
| | 1627 | 74 | | IFormattable f => f.ToString( null, CultureInfo.InvariantCulture ), |
| | 0 | 75 | | _ => value.ToString() ?? string.Empty |
| | 1627 | 76 | | }; |
| | | 77 | | } |