< Summary - ZhEaIsNsAaBn.Libraries coverage (affected: 23 packages)

Line coverage
93%
Covered lines: 306
Uncovered lines: 20
Coverable lines: 326
Total lines: 708
Line coverage: 93.8%
Branch coverage
77%
Covered branches: 142
Total branches: 183
Branch coverage: 77.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: .ctor()100%11100%
File 1: .cctor()100%11100%
File 1: EnsureTablesAsync()100%11100%
File 1: AcquireLockAsync()62.5%8890.9%
File 1: ReleaseLockAsync()100%11100%
File 1: ReadHistoryAsync()90%1010100%
File 1: UpsertAsync()50%22100%
File 1: DeleteAsync()50%22100%
File 1: SoftDeleteAsync(...)100%11100%
File 1: RestoreAsync(...)100%11100%
File 1: DeleteHistoryAsync()100%11100%
File 1: SetFlaggedAsync()50%44100%
File 1: CountReferencesAsync()91.66%1212100%
File 1: WriteHistoryAsync()100%11100%
File 1: WriteAuditAsync()100%11100%
File 1: Classify(...)100%11100%
File 1: LoadAsync()100%1010100%
File 1: TableColumnsAsync()100%1010100%
File 1: ReferencingKeysAsync()90%1010100%
File 1: Connection(...)50%22100%
File 1: Transaction(...)100%22100%
File 1: Command(...)100%11100%
File 1: NonQueryAsync()100%22100%
File 1: ScalarAsync()100%22100%
File 1: Parameter(...)100%44100%
File 1: Output(...)100%11100%
File 1: Values(...)100%11100%
File 1: Truncate(...)50%44100%
File 2: PlanForceDeleteAsync()100%11100%
File 2: ForceDeleteAsync()80%1010100%
File 2: DiscoverAsync()66.66%1212100%
File 2: PrimaryKeyAsync()66.66%66100%
File 2: NameAsync()50%88100%
File 2: DropAsync()100%22100%
File 2: .ctor()100%11100%
File 2: get_BottomUp()100%11100%
File 2: NewTemp()100%11100%
File 2: Add(...)100%11100%
File 2: Refuse(...)0%620%
File 3: Script(...)83.33%6691.17%
File 3: Step(...)57.14%8775%
File 3: Load(...)100%22100%
File 3: IdentityCandidates(...)100%11100%
File 3: Literal(...)100%22100%
File 3: Literal(...)63.33%2353038.88%
File 3: IsText(...)100%1212100%

File(s)

/home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.DbSeeder.SqlServer/SqlServerSeedDatabaseProvider.cs

#LineLine coverage
 1#region Usings
 2using ZhEaIsNsAaBn.Infrastructure.SqlServer;
 3using static ZhEaIsNsAaBn.DbSeeder.SqlServer.SqlServerSeedSql;
 4#endregion
 5
 6namespace ZhEaIsNsAaBn.DbSeeder.SqlServer;
 7
 8/// <summary>
 9/// SQL Server implementation of <see cref="ISeedDatabaseProvider"/> (plan §7): <c>sp_getapplock</c> session lock, bulk 
 10/// <c>#seed_n</c> then <c>UPDATE</c> + <c>INSERT … WHERE NOT EXISTS</c> (no <c>MERGE</c>), set-based deletes, foreign-k
 11/// <c>sys.foreign_keys</c>. Stateless; runs on the context's connection and transaction. Errors are classified by
 12/// <see cref="SqlServerDbErrorClassifier"/>.
 13/// </summary>
 14internal sealed partial class SqlServerSeedDatabaseProvider : ISeedDatabaseProvider
 15{
 16    private static long _tempTables;
 17
 18
 1419    private SqlServerSeedDatabaseProvider() { }
 20
 21
 722    public static SqlServerSeedDatabaseProvider Instance { get; } = new();
 23
 24
 25    public async Task EnsureTablesAsync( DbContext dbContext, bool async, CancellationToken cancellationToken )
 26    {
 33227        using var command = Command( dbContext, CreateHistory + "\n" + CreateAudit );
 28
 33229        await NonQueryAsync( command, async, cancellationToken ).ConfigureAwait( false );
 33230    }
 31
 32    public async Task<bool> AcquireLockAsync( DbContext dbContext, TimeSpan timeout, bool async, CancellationToken cance
 33    {
 32134        var milliseconds = timeout < TimeSpan.Zero ? -1 : (int) Math.Min( timeout.TotalMilliseconds, int.MaxValue );
 35
 32136        using var command = Command( dbContext, AcquireLock, Parameter( "@TimeoutMs", milliseconds ) );
 37
 38        // The command must outlive the lock wait.
 32139        command.CommandTimeout = milliseconds < 0 ? 0 : command.CommandTimeout + (int) Math.Ceiling( milliseconds / 1000
 40
 41
 32142        var result = System.Convert.ToInt32( await ScalarAsync( command, async, cancellationToken ).ConfigureAwait( fals
 43
 44        // 0 / 1 granted, -1 timeout; -2 cancelled, -3 deadlock victim, -999 parameter / call error.
 32145        return result switch
 32146                   {
 31747                       >= 0 => true,
 448                       -1   => false,
 049                       _    => throw new InvalidOperationException( $"sp_getapplock returned {result}." )
 32150                   };
 32151    }
 52
 53    public async Task ReleaseLockAsync( DbContext dbContext, bool async, CancellationToken cancellationToken )
 54    {
 31755        using var command = Command( dbContext, ReleaseLock );
 56
 31757        await NonQueryAsync( command, async, cancellationToken ).ConfigureAwait( false );
 31758    }
 59
 60    public async Task<IReadOnlyList<SeedHistoryRow>> ReadHistoryAsync( DbContext dbContext, string contextKey, bool asyn
 61    {
 34162        using ( var exists = Command( dbContext, HistoryExists ) )
 63        {
 34464            if ( System.Convert.ToInt32( await ScalarAsync( exists, async, cancellationToken ).ConfigureAwait( false ), 
 33865        }
 66
 67
 33868        using var command = Command( dbContext, ReadHistory, Parameter( "@ContextKey", contextKey ) );
 33869        using var reader  = async ? await command.ExecuteReaderAsync( cancellationToken ).ConfigureAwait( false ) : comm
 33870        var       rows    = new List<SeedHistoryRow>();
 71
 67572        while ( async ? await reader.ReadAsync( cancellationToken ).ConfigureAwait( false ) : reader.Read() )
 73        {
 33774            rows.Add( new SeedHistoryRow
 33775                          {
 33776                              MigrationId    = reader.GetString( 0 ),
 33777                              ContextKey     = reader.GetString( 1 ),
 33778                              Checksum       = reader.GetString( 2 ),
 33779                              AppliedOn      = reader.GetDateTime( 3 ),
 33780                              ProductVersion = reader.GetString( 4 )
 33781                          } );
 82        }
 83
 84
 33885        return rows;
 34186    }
 87
 88    public async Task<SeedWriteCounts> UpsertAsync( DbContext dbContext, SeedTableWrite write, bool async, CancellationT
 89    {
 114690        ArgumentNullException.ThrowIfNull( write );
 91
 114692        if ( write.Rows.Count == 0 ) { return default; }
 93
 94
 114695        var (temp, identity) = await LoadAsync( dbContext, write.Table, write.Columns, write.Rows, async, cancellationTo
 114696        var updated  = Output( "@Updated" );
 114697        var inserted = Output( "@Inserted" );
 98
 114699        using var command = Command( dbContext,
 1146100                                     Upsert( write, temp, identity ),
 1146101                                     [ updated, inserted, ..Values( "@u", write.UpdateValues ), ..Values( "@i", write.In
 102
 1146103        await NonQueryAsync( command, async, cancellationToken ).ConfigureAwait( false );
 104
 105
 1142106        return new SeedWriteCounts( (int) inserted.Value, (int) updated.Value );
 1142107    }
 108
 109    public async Task<int> DeleteAsync( DbContext dbContext, SeedTableKeys keys, bool async, CancellationToken cancellat
 110    {
 99111        ArgumentNullException.ThrowIfNull( keys );
 112
 99113        if ( keys.Keys.Count == 0 ) { return 0; }
 114
 115
 99116        var (temp, _) = await LoadAsync( dbContext, keys.Table, keys.Table.MatchColumns, keys.Keys, async, cancellationT
 99117        var count = Output( "@Count" );
 118
 99119        using var command = Command( dbContext, Delete( keys.Table, temp ), count );
 120
 99121        await NonQueryAsync( command, async, cancellationToken ).ConfigureAwait( false );
 122
 96123        return (int) count.Value;
 96124    }
 125
 126    public Task<int> SoftDeleteAsync( DbContext dbContext, SeedTableKeys keys, IReadOnlyList<SeedColumnValue> values, bo
 165127        => SetFlaggedAsync( dbContext, keys, values, flagged: false, async, cancellationToken );
 128
 129    public Task<int> RestoreAsync( DbContext dbContext, SeedTableKeys keys, IReadOnlyList<SeedColumnValue> values, bool 
 62130        => SetFlaggedAsync( dbContext, keys, values, flagged: true, async, cancellationToken );
 131
 132    public async Task DeleteHistoryAsync( DbContext dbContext, string migrationId, string contextKey, bool async, Cancel
 133    {
 96134        using var command = Command( dbContext, DeleteHistory, Parameter( "@MigrationId", migrationId ), Parameter( "@Co
 135
 96136        await NonQueryAsync( command, async, cancellationToken ).ConfigureAwait( false );
 96137    }
 138
 139    private static async Task<int> SetFlaggedAsync( DbContext dbContext, SeedTableKeys keys, IReadOnlyList<SeedColumnVal
 140    {
 227141        ArgumentNullException.ThrowIfNull( keys );
 227142        ArgumentNullException.ThrowIfNull( values );
 143
 227144        if ( keys.Table.SoftDeleteFlag is null ) { throw new InvalidOperationException( $"{keys.Table.DisplayName} has n
 145
 227146        if ( keys.Keys.Count == 0 ) { return 0; }
 147
 148
 227149        var (temp, _) = await LoadAsync( dbContext, keys.Table, keys.Table.MatchColumns, keys.Keys, async, cancellationT
 227150        var count = Output( "@Count" );
 151
 227152        using var command = Command( dbContext, SetFlagged( keys.Table, temp, values, flagged ), [ count, ..Values( "@v"
 153
 227154        await NonQueryAsync( command, async, cancellationToken ).ConfigureAwait( false );
 155
 227156        return (int) count.Value;
 227157    }
 158
 159    public async Task<IReadOnlyList<SeedBlocker>> CountReferencesAsync( DbContext dbContext, SeedTableKeys keys, bool as
 160    {
 150161        ArgumentNullException.ThrowIfNull( keys );
 162
 150163        if ( keys.Keys.Count == 0 ) { return []; }
 164
 165
 150166        var foreignKeys = await ReferencingKeysAsync( dbContext, Table( keys.Table ), async, cancellationToken ).Configu
 167
 213168        if ( foreignKeys.Count == 0 ) { return []; }
 169
 170
 87171        var (temp, _)  = await LoadAsync( dbContext, keys.Table, keys.Table.MatchColumns, keys.Keys, async, cancellation
 87172        var blockers = new List<SeedBlocker>();
 173
 348174        foreach ( var foreignKey in foreignKeys )
 175        {
 87176            var referencing = $"{Quote( foreignKey.Schema )}.{Quote( foreignKey.Table )}";
 87177            var isSelf      = string.Equals( foreignKey.Table, keys.Table.Name, StringComparison.Ordinal )
 87178                           && string.Equals( foreignKey.Schema, keys.Table.Schema ?? foreignKey.Schema, StringComparison
 179
 87180            using var command = Command( dbContext, CountReferences( keys.Table, temp, referencing, foreignKey.Pairs, is
 181
 87182            var count = System.Convert.ToInt32( await ScalarAsync( command, async, cancellationToken ).ConfigureAwait( f
 183
 144184            if ( count > 0 ) { blockers.Add( new SeedBlocker( $"{foreignKey.Schema}.{foreignKey.Table}", string.Join( ",
 87185        }
 186
 187
 261188        using ( var drop = Command( dbContext, $"DROP TABLE {temp};" ) ) { await NonQueryAsync( drop, async, cancellatio
 189
 87190        return blockers;
 150191    }
 192
 193    public async Task WriteHistoryAsync( DbContext dbContext, SeedHistoryRow row, bool async, CancellationToken cancella
 194    {
 281195        ArgumentNullException.ThrowIfNull( row );
 196
 281197        using var command = Command( dbContext,
 281198                                     InsertHistory,
 281199                                     Parameter( "@MigrationId", row.MigrationId ),
 281200                                     Parameter( "@ContextKey", row.ContextKey ),
 281201                                     Parameter( "@Checksum", row.Checksum ),
 281202                                     Parameter( "@AppliedOn", row.AppliedOn ),
 281203                                     Parameter( "@ProductVersion", row.ProductVersion ) );
 204
 281205        await NonQueryAsync( command, async, cancellationToken ).ConfigureAwait( false );
 281206    }
 207
 208    public async Task WriteAuditAsync( DbContext dbContext, SeedAuditRow row, bool async, CancellationToken cancellation
 209    {
 327210        ArgumentNullException.ThrowIfNull( row );
 211
 327212        using var command = Command( dbContext,
 327213                                     InsertAudit,
 327214                                     Parameter( "@MigrationId", row.MigrationId ),
 327215                                     Parameter( "@ContextKey", row.ContextKey ),
 327216                                     Parameter( "@Operation", row.Operation ),
 327217                                     Parameter( "@StartedOn", row.StartedOn ),
 327218                                     Parameter( "@FinishedOn", row.FinishedOn ),
 327219                                     Parameter( "@Succeeded", row.Succeeded ),
 327220                                     Parameter( "@ErrorCode", row.ErrorCode ),
 327221                                     Parameter( "@RowsInserted", row.RowsInserted ),
 327222                                     Parameter( "@RowsUpdated", row.RowsUpdated ),
 327223                                     Parameter( "@RowsDeleted", row.RowsDeleted ),
 327224                                     Parameter( "@OsUser", Truncate( row.OsUser ) ),
 327225                                     Parameter( "@MachineName", Truncate( row.MachineName ) ),
 327226                                     Parameter( "@ToolVersion", row.ToolVersion ),
 327227                                     Parameter( "@Details", row.Details ) );
 228
 327229        await NonQueryAsync( command, async, cancellationToken ).ConfigureAwait( false );
 327230    }
 231
 7232    public Error? Classify( Exception exception ) => SqlServerDbErrorClassifier.Instance.Classify( exception );
 233
 234
 235    // ---- helpers ----
 236
 237    /// Creates <c>#seed_n</c> for <paramref name="columns"/> (collations from the target table) and bulk-copies the row
 238    /// <returns>The temp table name and the target's identity columns.</returns>
 239    private static async Task<(string Temp, IReadOnlySet<string> Identity)> LoadAsync(
 240            DbContext dbContext,
 241            SeedTable table,
 242            IReadOnlyList<SeedColumn> columns,
 243            IReadOnlyList<IReadOnlyList<object?>> rows,
 244            bool async,
 245            CancellationToken cancellationToken
 246        )
 247    {
 1595248        var (collations, identity) = await TableColumnsAsync( dbContext, table, async, cancellationToken ).ConfigureAwai
 1595249        var temp                   = $"#seed_{Interlocked.Increment( ref _tempTables ).ToString( CultureInfo.InvariantCu
 250
 4785251        using ( var create = Command( dbContext, CreateTemp( temp, columns, collations ) ) ) { await NonQueryAsync( crea
 252
 253
 1595254        using var data = new DataTable { Locale = CultureInfo.InvariantCulture };
 255
 15355256        foreach ( var column in columns ) { data.Columns.Add( column.ColumnName, Nullable.GetUnderlyingType( column.Prov
 257
 10270258        foreach ( var row in rows ) { data.Rows.Add( row.Select( v => v ?? DBNull.Value ).ToArray() ); }
 259
 260
 1595261        using var bulk = new SqlBulkCopy( Connection( dbContext ), SqlBulkCopyOptions.Default, Transaction( dbContext ) 
 1595262                             {
 1595263                                 DestinationTableName = temp,
 1595264                                 BulkCopyTimeout      = dbContext.Database.GetCommandTimeout() ?? 30
 1595265                             };
 266
 15355267        foreach ( var column in columns ) { bulk.ColumnMappings.Add( column.ColumnName, column.ColumnName ); }
 268
 269
 3110270        if ( async ) { await bulk.WriteToServerAsync( data, cancellationToken ).ConfigureAwait( false ); }
 80271        else { bulk.WriteToServer( data ); }
 272
 1595273        return ( temp, identity );
 1595274    }
 275
 276    private static async Task<(IReadOnlyDictionary<string, string> Collations, IReadOnlySet<string> Identity)> TableColu
 277            DbContext dbContext,
 278            SeedTable table,
 279            bool async,
 280            CancellationToken cancellationToken
 281        )
 282    {
 1595283        using var command    = Command( dbContext, TableColumns, Parameter( "@Table", Table( table ) ) );
 1595284        using var reader     = async ? await command.ExecuteReaderAsync( cancellationToken ).ConfigureAwait( false ) : c
 1595285        var       collations = new Dictionary<string, string>( StringComparer.Ordinal );
 1595286        var       identity   = new HashSet<string>( StringComparer.Ordinal );
 287
 13698288        while ( async ? await reader.ReadAsync( cancellationToken ).ConfigureAwait( false ) : reader.Read() )
 289        {
 17135290            if ( !reader.IsDBNull( 1 ) ) { collations[ reader.GetString( 0 ) ] = reader.GetString( 1 ); }
 291
 12382292            if ( reader.GetBoolean( 2 ) ) { identity.Add( reader.GetString( 0 ) ); }
 293        }
 294
 295
 1595296        return ( collations, identity );
 1595297    }
 298
 299    private static async Task<IReadOnlyList<(string Schema, string Table, IReadOnlyList<(string Column, string Reference
 300            DbContext dbContext,
 301            string quotedTable,
 302            bool async,
 303            CancellationToken cancellationToken
 304        )
 305    {
 258306        using var command = Command( dbContext, ReferencingKeys, Parameter( "@Table", quotedTable ) );
 258307        using var reader  = async ? await command.ExecuteReaderAsync( cancellationToken ).ConfigureAwait( false ) : comm
 258308        var       keys    = new List<(int Id, string Schema, string Table, List<(string, string)> Pairs)>();
 309
 417310        while ( async ? await reader.ReadAsync( cancellationToken ).ConfigureAwait( false ) : reader.Read() )
 311        {
 159312            var id = reader.GetInt32( 0 );
 313
 318314            if ( keys.Count == 0 || keys[ ^1 ].Id != id ) { keys.Add( ( id, reader.GetString( 1 ), reader.GetString( 2 )
 315
 159316            keys[ ^1 ].Pairs.Add( ( reader.GetString( 3 ), reader.GetString( 4 ) ) );
 317        }
 318
 319
 258320        return keys.Select( k => (k.Schema, k.Table, (IReadOnlyList<(string, string)>) k.Pairs) ).ToList();
 258321    }
 322
 323    private static SqlConnection Connection( DbContext dbContext )
 9462324        => dbContext.Database.GetDbConnection() as SqlConnection
 9462325        ?? throw new InvalidOperationException( "ZhEaIsNsAaBn.DbSeeder.SqlServer needs a SQL Server context (UseSqlServe
 326
 9462327    private static SqlTransaction? Transaction( DbContext dbContext ) => dbContext.Database.CurrentTransaction?.GetDbTra
 328
 329    [ System.Diagnostics.CodeAnalysis.SuppressMessage( "Security", "CA2100:Review SQL queries for security vulnerabiliti
 330                                                       Justification = "SQL is built by SqlServerSeedSql from EF / datab
 331    private static SqlCommand Command( DbContext dbContext, string sql, params SqlParameter[] parameters )
 332    {
 7867333        var command = Connection( dbContext ).CreateCommand();
 334
 7867335        command.CommandText    = sql;
 7867336        command.Transaction    = Transaction( dbContext );
 7867337        command.CommandTimeout = dbContext.Database.GetCommandTimeout() ?? 30;
 7867338        command.Parameters.AddRange( parameters );
 339
 7867340        return command;
 341    }
 342
 343    private static async Task NonQueryAsync( SqlCommand command, bool async, CancellationToken cancellationToken )
 344    {
 9154345        if ( async ) { await command.ExecuteNonQueryAsync( cancellationToken ).ConfigureAwait( false ); }
 220346        else { command.ExecuteNonQuery(); }
 4680347    }
 348
 349    private static async Task<object?> ScalarAsync( SqlCommand command, bool async, CancellationToken cancellationToken 
 845350        => async ? await command.ExecuteScalarAsync( cancellationToken ).ConfigureAwait( false ) : command.ExecuteScalar
 351
 352    /// A typed parameter; dates as <c>datetime2</c> so no precision is lost before the column's own conversion.
 353    private static SqlParameter Parameter( string name, object? value )
 354    {
 12252355        var parameter = new SqlParameter( name, value ?? DBNull.Value );
 356
 14406357        if ( value is DateTime ) { parameter.SqlDbType = SqlDbType.DateTime2; }
 358
 12252359        return parameter;
 360    }
 361
 2618362    private static SqlParameter Output( string name ) => new( name, SqlDbType.Int ) { Direction = ParameterDirection.Out
 363
 364    private static IEnumerable<SqlParameter> Values( string prefix, IReadOnlyList<SeedColumnValue> values )
 2519365        => values.Select( ( v, i ) => Parameter( prefix + i.ToString( CultureInfo.InvariantCulture ), v.Value ) );
 366
 654367    private static string? Truncate( string? value ) => value is { Length: > 256 } ? value[ ..256 ] : value;
 368}

/home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.DbSeeder.SqlServer/SqlServerSeedDatabaseProvider.Force.cs

#LineLine coverage
 1#region Usings
 2using static ZhEaIsNsAaBn.DbSeeder.SqlServer.SqlServerSeedSql;
 3#endregion
 4
 5namespace ZhEaIsNsAaBn.DbSeeder.SqlServer;
 6
 7/// The design-time forced delete: the full recursive tree of referencing rows, from <c>sys.foreign_keys</c>.
 8internal sealed partial class SqlServerSeedDatabaseProvider
 9{
 10    public async Task<SeedForcePlan> PlanForceDeleteAsync( DbContext dbContext, SeedTableKeys keys, bool async, Cancella
 11    {
 3012        ArgumentNullException.ThrowIfNull( keys );
 13
 3014        var tree = await DiscoverAsync( dbContext, keys, async, cancellationToken ).ConfigureAwait( false );
 15
 3016        await DropAsync( dbContext, tree.Temps, async, cancellationToken ).ConfigureAwait( false );
 17
 3018        return new SeedForcePlan { Nodes = tree.BottomUp.Select( n => n.Node ).ToList(), Refusal = tree.Refusal };
 3019    }
 20
 21    public async Task<IReadOnlyList<SeedForceDeleted>> ForceDeleteAsync( DbContext dbContext, SeedTableKeys keys, bool a
 22    {
 623        ArgumentNullException.ThrowIfNull( keys );
 24
 625        var tree = await DiscoverAsync( dbContext, keys, async, cancellationToken ).ConfigureAwait( false );
 26
 27        try
 28        {
 629            if ( tree.Refusal is not null ) { throw new InvalidOperationException( tree.Refusal ); }
 30
 31
 632            var deleted = new List<SeedForceDeleted>();
 33
 3634            foreach ( var (node, quoted, primaryKey, selection) in tree.BottomUp )
 35            {
 1236                var join = string.Join( " AND ", primaryKey.Select( c => $"c.{Quote( c )} = f.{Quote( c )}" ) );
 37
 1238                using var capture = Command( dbContext, $"SELECT ( SELECT c.* FROM {quoted} AS c INNER JOIN {selection} 
 1239                var       json    = await ScalarAsync( capture, async, cancellationToken ).ConfigureAwait( false ) as st
 40
 1241                using var delete = Command( dbContext, $"DELETE c FROM {quoted} AS c INNER JOIN {selection} AS f ON {joi
 1242                var       rows   = Convert.ToInt32( await ScalarAsync( delete, async, cancellationToken ).ConfigureAwait
 43
 1244                deleted.Add( new SeedForceDeleted( node.Table, rows, json ) );
 1245            }
 46
 47
 648            return deleted;
 49        }
 50        finally
 51        {
 652            await DropAsync( dbContext, tree.Temps, async, cancellationToken ).ConfigureAwait( false );
 53        }
 654    }
 55
 56
 57    /// Breadth-first over the foreign keys pointing at each selected table; each level's rows are kept (by primary key)
 58    private static async Task<ForceTree> DiscoverAsync( DbContext dbContext, SeedTableKeys keys, bool async, Cancellatio
 59    {
 3660        var tree = new ForceTree();
 3661        var root = await NameAsync( dbContext, Table( keys.Table ), async, cancellationToken ).ConfigureAwait( false );
 3662        var pk   = await PrimaryKeyAsync( dbContext, Table( keys.Table ), async, cancellationToken ).ConfigureAwait( fal
 63
 3664        if ( pk.Count == 0 ) { return tree.Refuse( $"{root} has no primary key." ); }
 65
 66
 3667        var (keyTemp, _) = await LoadAsync( dbContext, keys.Table, keys.Table.MatchColumns, keys.Keys, async, cancellati
 3668        tree.Temps.Add( keyTemp );
 69
 3670        var rootSelection = tree.NewTemp();
 71
 3672        using ( var select = Command( dbContext, $"SELECT DISTINCT {string.Join( ", ", pk.Select( c => $"t.{Quote( c )}"
 73        {
 3674            await NonQueryAsync( select, async, cancellationToken ).ConfigureAwait( false );
 3675        }
 76
 77
 3678        var queue = new Queue<(string Name, string Quoted, IReadOnlyList<string> Pk, string Selection, int Depth, IReadO
 3679        queue.Enqueue( ( root, Table( keys.Table ), pk, rootSelection, 0, new HashSet<string>( [ root ], StringComparer.
 80
 14481        while ( queue.Count > 0 )
 82        {
 10883            var parent = queue.Dequeue();
 84
 36085            foreach ( var foreignKey in await ReferencingKeysAsync( dbContext, parent.Quoted, async, cancellationToken )
 86            {
 7287                var child       = $"{foreignKey.Schema}.{foreignKey.Table}";
 7288                var childQuoted = $"{Quote( foreignKey.Schema )}.{Quote( foreignKey.Table )}";
 7289                var childPk     = await PrimaryKeyAsync( dbContext, childQuoted, async, cancellationToken ).ConfigureAwa
 90
 7291                if ( childPk.Count == 0 ) { return tree.Refuse( $"{child} references {parent.Name} but has no primary ke
 92
 93
 7294                var selection = tree.NewTemp();
 7295                var through   = string.Join( " AND ", foreignKey.Pairs.Select( p => $"c.{Quote( p.Column )} = p.{Quote( 
 7296                var byKey     = string.Join( " AND ", parent.Pk.Select( c => $"p.{Quote( c )} = f.{Quote( c )}" ) );
 97
 7298                using var select = Command( dbContext,
 7299                                            $"SELECT DISTINCT {string.Join( ", ", childPk.Select( c => $"c.{Quote( c )}"
 72100                                            $"INNER JOIN {parent.Quoted} AS p ON {through} INNER JOIN {parent.Selection}
 101
 72102                var rows = Convert.ToInt32( await ScalarAsync( select, async, cancellationToken ).ConfigureAwait( false 
 103
 72104                if ( rows == 0 ) { continue; }
 105
 106                // A table already on the path (a cycle, or a table referencing itself) has no safe bottom-up order.
 72107                if ( parent.Path.Contains( child ) ) { return tree.Refuse( $"{child} references {parent.Name} in a cycle
 108
 109
 72110                tree.Add( new SeedForceNode( parent.Depth + 1, child, string.Join( ", ", foreignKey.Pairs.Select( p => p
 72111                queue.Enqueue( ( child, childQuoted, childPk, selection, parent.Depth + 1, new HashSet<string>( parent.P
 72112            }
 108113        }
 114
 115
 36116        return tree;
 36117    }
 118
 119    private static async Task<IReadOnlyList<string>> PrimaryKeyAsync( DbContext dbContext, string quotedTable, bool asyn
 120    {
 108121        using var command = Command( dbContext, PrimaryKeyColumns, Parameter( "@Table", quotedTable ) );
 108122        using var reader  = async ? await command.ExecuteReaderAsync( cancellationToken ).ConfigureAwait( false ) : comm
 108123        var       columns = new List<string>();
 124
 324125        while ( async ? await reader.ReadAsync( cancellationToken ).ConfigureAwait( false ) : reader.Read() ) { columns.
 126
 108127        return columns;
 108128    }
 129
 130    private static async Task<string> NameAsync( DbContext dbContext, string quotedTable, bool async, CancellationToken 
 131    {
 36132        using var command = Command( dbContext, TableName, Parameter( "@Table", quotedTable ) );
 36133        using var reader  = async ? await command.ExecuteReaderAsync( cancellationToken ).ConfigureAwait( false ) : comm
 134
 36135        return ( async ? await reader.ReadAsync( cancellationToken ).ConfigureAwait( false ) : reader.Read() ) && !reade
 36136                   ? $"{reader.GetString( 0 )}.{reader.GetString( 1 )}"
 36137                   : quotedTable;
 36138    }
 139
 140    private static async Task DropAsync( DbContext dbContext, IEnumerable<string> temps, bool async, CancellationToken c
 141    {
 360142        foreach ( var temp in temps )
 143        {
 144144            using var drop = Command( dbContext, $"IF OBJECT_ID( N'tempdb..{temp}' ) IS NOT NULL DROP TABLE {temp};" );
 145
 144146            await NonQueryAsync( drop, async, cancellationToken ).ConfigureAwait( false );
 144147        }
 36148    }
 149
 150
 151    /// The discovered tree: nodes with their row selections (temp tables), and every temp table to drop.
 152    private sealed class ForceTree
 153    {
 36154        private readonly List<(SeedForceNode Node, string Quoted, IReadOnlyList<string> Pk, string Selection)> _nodes = 
 155
 156
 157        public List<string> Temps { get; } = [];
 158
 159        public string? Refusal { get; private set; }
 160
 161        /// Deepest first; within a depth, the last found first.
 162        public IEnumerable<(SeedForceNode Node, string Quoted, IReadOnlyList<string> Pk, string Selection)> BottomUp
 36163            => _nodes.Select( ( n, i ) => (n, i) ).OrderByDescending( x => x.n.Node.Depth ).ThenByDescending( x => x.i )
 164
 165
 166        public string NewTemp()
 167        {
 108168            var temp = $"#force_{Interlocked.Increment( ref _tempTables ).ToString( CultureInfo.InvariantCulture )}";
 108169            Temps.Add( temp );
 170
 108171            return temp;
 172        }
 173
 72174        public void Add( SeedForceNode node, string quoted, IReadOnlyList<string> pk, string selection ) => _nodes.Add( 
 175
 176        public ForceTree Refuse( string reason )
 177        {
 0178            Refusal ??= reason;
 179
 0180            return this;
 181        }
 182    }
 183}

/home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.DbSeeder.SqlServer/SqlServerSeedDatabaseProvider.Script.cs

#LineLine coverage
 1#region Usings
 2using Microsoft.EntityFrameworkCore.Metadata;
 3using static ZhEaIsNsAaBn.DbSeeder.SqlServer.SqlServerSeedSql;
 4#endregion
 5
 6namespace ZhEaIsNsAaBn.DbSeeder.SqlServer;
 7
 8/// <summary>
 9/// The idempotent SQL script for a DBA: the same statements as the runner (temp table, null-safe <c>UPDATE</c>, <c>INSE
 10/// EXISTS</c>, set-based deletes / soft deletes / restores) with the rows as literals. Per migration: one transaction u
 11/// skipped when the history already has it; history and audit rows written (<c>SUSER_SNAME()</c>); audit dates are the 
 12/// </summary>
 13internal sealed partial class SqlServerSeedDatabaseProvider
 14{
 15    private const int RowsPerInsert = 900;
 16
 17
 18    public string Script( IReadOnlyList<SeedScriptMigration> migrations )
 19    {
 320        ArgumentNullException.ThrowIfNull( migrations );
 21
 322        var sql  = new StringBuilder();
 323        var temp = 0;
 24
 25
 326        sql.AppendLine( "-- ZhEaIsNsAaBn.DbSeeder seed script. Idempotent: a migration already in [__SeedMigrationsHisto
 327           .AppendLine( "SET NOCOUNT ON;" )
 328           .AppendLine( "SET XACT_ABORT ON;" )
 329           .AppendLine()
 330           .AppendLine( CreateHistory )
 331           .AppendLine( CreateAudit )
 332           .AppendLine()
 333           .AppendLine( "DECLARE @Now datetime2, @StartedOn datetime2, @Inserted int, @Updated int, @Count int, @TotalIn
 34
 35
 1836        foreach ( var migration in migrations )
 37        {
 638            var id  = Text( migration.Id );
 639            var key = Text( migration.ContextKey );
 40
 641            sql.AppendLine()
 642               .AppendLine( CultureInfo.InvariantCulture, $"-- Seed migration {migration.Id}" )
 643               .AppendLine( "BEGIN TRANSACTION;" )
 644               .AppendLine( CultureInfo.InvariantCulture, $"EXEC sp_getapplock @Resource = N'{LockResource}', @LockMode 
 645               .AppendLine( CultureInfo.InvariantCulture, $"IF NOT EXISTS ( SELECT 1 FROM [{HistoryTable}] WHERE [Migrat
 646               .AppendLine( "BEGIN" );
 47
 648            if ( migration.RequiresSchemaMigration is { } schema )
 49            {
 050                var message = Text( $"SchemaNotReady: {migration.Id} needs the EF migration {schema}." );
 51
 052                sql.AppendLine( CultureInfo.InvariantCulture, $"    IF OBJECT_ID( N'[__EFMigrationsHistory]', N'U' ) IS 
 053                   .AppendLine( CultureInfo.InvariantCulture, $"    IF NOT EXISTS ( SELECT 1 FROM [__EFMigrationsHistory
 54            }
 55
 656            sql.AppendLine( "    SELECT @Now = SYSDATETIME(), @StartedOn = SYSDATETIME(), @TotalInserted = 0, @TotalUpda
 57
 58
 8459            foreach ( var step in migration.Steps ) { Step( sql, step, $"#script_{++temp}" ); }
 60
 61
 662            sql.AppendLine( CultureInfo.InvariantCulture, $"    INSERT INTO [{HistoryTable}] ( [MigrationId], [ContextKe
 663               .AppendLine( CultureInfo.InvariantCulture, $"    VALUES ( {id}, {key}, {Text( migration.Checksum )}, @Now
 664               .AppendLine( CultureInfo.InvariantCulture, $"    INSERT INTO [{AuditTable}] ( [MigrationId], [ContextKey]
 665               .AppendLine( "                                 [RowsInserted], [RowsUpdated], [RowsDeleted], [DbLogin], [
 666               .AppendLine( CultureInfo.InvariantCulture, $"    VALUES ( {id}, {key}, N'Up', @StartedOn, SYSDATETIME(), 
 667               .AppendLine( "END;" )
 668               .AppendLine( "COMMIT TRANSACTION;" );
 69        }
 70
 71
 372        return sql.ToString();
 73    }
 74
 75
 76    private static void Step( StringBuilder sql, SeedStep step, string temp )
 77    {
 78        switch ( step.Kind )
 79        {
 80            case SeedStepKind.Upsert:
 2181                var write = step.Write!;
 82
 2183                Load( sql, temp, write.Columns, write.Rows );
 2184                sql.AppendLine( Upsert( write, temp, IdentityCandidates( write ), i => Literal( write.UpdateValues[ i ] 
 2185                   .AppendLine( "SET @TotalInserted += @Inserted; SET @TotalUpdated += @Updated;" );
 86
 2187                break;
 88
 89            case SeedStepKind.Restore or SeedStepKind.SoftDelete:
 390                Load( sql, temp, step.Table.MatchColumns, step.Keys!.Keys );
 391                sql.AppendLine( SetFlagged( step.Table, temp, step.Values, flagged: step.Kind == SeedStepKind.Restore, i
 392                   .AppendLine( step.Kind == SeedStepKind.Restore ? "SET @TotalUpdated += @Count;" : "SET @TotalDeleted 
 93
 394                break;
 95
 96            case SeedStepKind.Delete:
 97                // The database's foreign keys refuse a referenced row (the error rolls the migration back).
 098                Load( sql, temp, step.Table.MatchColumns, step.Keys!.Keys );
 099                sql.AppendLine( Delete( step.Table, temp ) ).AppendLine( "SET @TotalDeleted += @Count;" );
 100
 101                break;
 102        }
 0103    }
 104
 105    /// <c>CREATE TABLE #temp</c> + the rows as <c>INSERT … VALUES</c> (900 per statement; SQL Server allows 1000).
 106    private static void Load( StringBuilder sql, string temp, IReadOnlyList<SeedColumn> columns, IReadOnlyList<IReadOnly
 107    {
 108        // No catalog at generation time: text columns take the database default collation.
 24109        var collations = columns.Where( c => IsText( c.StoreType ) ).ToDictionary( c => c.ColumnName, _ => "DATABASE_DEF
 24110        var names      = string.Join( ", ", columns.Select( c => Quote( c.ColumnName ) ) );
 111
 24112        sql.AppendLine( CreateTemp( temp, columns, collations ) );
 113
 96114        foreach ( var chunk in rows.Chunk( RowsPerInsert ) )
 115        {
 24116            sql.AppendLine( CultureInfo.InvariantCulture, $"INSERT INTO {temp} ( {names} ) VALUES" )
 24117               .AppendLine( string.Join( ",\n", chunk.Select( r => "    ( " + string.Join( ", ", r.Select( Literal ) ) +
 118        }
 24119    }
 120
 121    /// The model's identity candidates (generated-on-add key columns): never updated, and <c>IDENTITY_INSERT</c> is swi
 122    /// only when the target really has one (checked when the script runs).
 123    private static HashSet<string> IdentityCandidates( SeedTableWrite write )
 21124        => write.Columns.Where( c => c.Property.IsPrimaryKey() && c.Property.ValueGenerated == ValueGenerated.OnAdd ).Se
 125
 60126    private static string Literal( SeedColumnValue value ) => value.IsClock ? "@Now" : Literal( value.Value );
 127
 128    /// A provider value as a T-SQL literal (invariant formats; text Unicode with quotes doubled).
 129    private static string Literal( object? value )
 153130        => value switch
 153131               {
 3132                   null                => "NULL",
 78133                   string text         => Text( text ),
 0134                   char character      => Text( character.ToString() ),
 9135                   bool flag           => flag ? "1" : "0",
 0136                   DateTime date       => $"CONVERT( datetime2, N'{date.ToString( "yyyy-MM-ddTHH:mm:ss.fffffff", Culture
 0137                   DateTimeOffset date => $"CONVERT( datetimeoffset, N'{date.ToString( "yyyy-MM-ddTHH:mm:ss.fffffffzzz",
 0138                   DateOnly date       => $"N'{date.ToString( "yyyy-MM-dd", CultureInfo.InvariantCulture )}'",
 0139                   TimeOnly time       => $"N'{time.ToString( "HH:mm:ss.fffffff", CultureInfo.InvariantCulture )}'",
 0140                   TimeSpan span       => $"N'{span.ToString( "c", CultureInfo.InvariantCulture )}'",
 0141                   Guid guid           => $"N'{guid:D}'",
 0142                   byte[] bytes        => "0x" + Convert.ToHexString( bytes ),
 0143                   double number       => number.ToString( "R", CultureInfo.InvariantCulture ),
 0144                   float number        => number.ToString( "R", CultureInfo.InvariantCulture ),
 63145                   IFormattable number => number.ToString( null, CultureInfo.InvariantCulture ),
 0146                   _                   => throw new NotSupportedException( $"A seed value of type {value.GetType().Name}
 153147               };
 148
 149    private static bool IsText( string storeType )
 150    {
 72151        var type = storeType.TrimStart().ToUpperInvariant();
 152
 72153        return type.StartsWith( "NVARCHAR", StringComparison.Ordinal ) || type.StartsWith( "VARCHAR", StringComparison.O
 72154            || type.StartsWith( "CHAR", StringComparison.Ordinal ) || type.StartsWith( "NTEXT", StringComparison.Ordinal
 72155            || type == "SYSNAME";
 156    }
 157}

Methods/Properties

.ctor()
.cctor()
EnsureTablesAsync()
AcquireLockAsync()
ReleaseLockAsync()
ReadHistoryAsync()
UpsertAsync()
DeleteAsync()
SoftDeleteAsync(Microsoft.EntityFrameworkCore.DbContext,ZhEaIsNsAaBn.DbSeeder.SeedTableKeys,System.Collections.Generic.IReadOnlyList`1<ZhEaIsNsAaBn.DbSeeder.SeedColumnValue>,System.Boolean,System.Threading.CancellationToken)
RestoreAsync(Microsoft.EntityFrameworkCore.DbContext,ZhEaIsNsAaBn.DbSeeder.SeedTableKeys,System.Collections.Generic.IReadOnlyList`1<ZhEaIsNsAaBn.DbSeeder.SeedColumnValue>,System.Boolean,System.Threading.CancellationToken)
DeleteHistoryAsync()
SetFlaggedAsync()
CountReferencesAsync()
WriteHistoryAsync()
WriteAuditAsync()
Classify(System.Exception)
LoadAsync()
TableColumnsAsync()
ReferencingKeysAsync()
Connection(Microsoft.EntityFrameworkCore.DbContext)
Transaction(Microsoft.EntityFrameworkCore.DbContext)
Command(Microsoft.EntityFrameworkCore.DbContext,System.String,Microsoft.Data.SqlClient.SqlParameter[])
NonQueryAsync()
ScalarAsync()
Parameter(System.String,System.Object)
Output(System.String)
Values(System.String,System.Collections.Generic.IReadOnlyList`1<ZhEaIsNsAaBn.DbSeeder.SeedColumnValue>)
Truncate(System.String)
PlanForceDeleteAsync()
ForceDeleteAsync()
DiscoverAsync()
PrimaryKeyAsync()
NameAsync()
DropAsync()
.ctor()
get_BottomUp()
NewTemp()
Add(ZhEaIsNsAaBn.DbSeeder.SeedForceNode,System.String,System.Collections.Generic.IReadOnlyList`1<System.String>,System.String)
Refuse(System.String)
Script(System.Collections.Generic.IReadOnlyList`1<ZhEaIsNsAaBn.DbSeeder.SeedScriptMigration>)
Step(System.Text.StringBuilder,ZhEaIsNsAaBn.DbSeeder.SeedStep,System.String)
Load(System.Text.StringBuilder,System.String,System.Collections.Generic.IReadOnlyList`1<ZhEaIsNsAaBn.DbSeeder.SeedColumn>,System.Collections.Generic.IReadOnlyList`1<System.Collections.Generic.IReadOnlyList`1<System.Object>>)
IdentityCandidates(ZhEaIsNsAaBn.DbSeeder.SeedTableWrite)
Literal(ZhEaIsNsAaBn.DbSeeder.SeedColumnValue)
Literal(System.Object)
IsText(System.String)