• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / SchemaSaurus / 27082301842

07 Jun 2026 04:09AM UTC coverage: 86.906% (-0.1%) from 87.024%
27082301842

push

github

pwelter34
Merge branch 'main' of https://github.com/loresoft/SchemaSaurus

1065 of 1430 branches covered (74.48%)

Branch coverage included in aggregate %.

5738 of 6398 relevant lines covered (89.68%)

227.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.17
/src/SchemaSaurus.SqlServer/SqlServerSchemaReader.Tables.cs
1
using Microsoft.Data.SqlClient;
2

3
using SchemaSaurus.Metadata;
4
using SchemaSaurus.Metadata.Builders;
5
using SchemaSaurus.Metadata.Extensions;
6
using SchemaSaurus.Metadata.Provider;
7

8
namespace SchemaSaurus.SqlServer;
9

10
public sealed partial class SqlServerSchemaReader
11
{
12
    /// <inheritdoc />
13
    protected override async Task ReadTablesAsync(
14
        SqlConnection connection,
15
        DatabaseModelBuilder builder,
16
        SchemaReaderOptions options,
17
        CancellationToken cancellationToken)
18
    {
19
        var tableFilter = BuildTableFilter(options);
7✔
20

21
        var tables = await ReadTableDefinitionsAsync(connection, tableFilter, cancellationToken).ConfigureAwait(false);
7✔
22
        if (tables.Count == 0)
7!
23
            return;
×
24

25
        await ReadTableColumnsAsync(connection, tables, tableFilter, cancellationToken).ConfigureAwait(false);
7✔
26
        await ReadKeyConstraintsAsync(connection, tables, cancellationToken).ConfigureAwait(false);
7✔
27
        await ReadTableIndexesAsync(connection, tables, cancellationToken).ConfigureAwait(false);
7✔
28
        await ReadCheckConstraintsAsync(connection, tables, cancellationToken).ConfigureAwait(false);
7✔
29
        await ReadTableForeignKeysAsync(connection, tables, cancellationToken).ConfigureAwait(false);
7✔
30
        await ReadTableTriggersAsync(connection, tables, cancellationToken).ConfigureAwait(false);
7✔
31

32
        // Build the tables and add them to the DatabaseModelBuilder. We do this at the end after reading all related metadata to
33
        // ensure that all properties (including extended properties) are applied to the TableBuilder before it's built.
34
        foreach (var (_, tb) in tables)
424✔
35
            builder.AddTable(tb.Build());
205✔
36
    }
7✔
37

38
    private async Task<Dictionary<int, TableBuilder>> ReadTableDefinitionsAsync(
39
        SqlConnection connection,
40
        string tableFilter,
41
        CancellationToken cancellationToken)
42
    {
43
        var tables = new Dictionary<int, TableBuilder>();
7✔
44
        var sql = $"""
7✔
45
            SELECT
7✔
46
                t.object_id,
7✔
47
                SCHEMA_NAME(t.schema_id)            AS schema_name,
7✔
48
                t.name                              AS table_name,
7✔
49
                t.temporal_type,
7✔
50
                t.is_memory_optimized,
7✔
51
                t.is_filetable,
7✔
52
                SCHEMA_NAME(ht.schema_id)           AS history_schema,
7✔
53
                ht.name                             AS history_name,
7✔
54
                pc.start_column_name,
7✔
55
                pc.end_column_name,
7✔
56
                CAST(ep.value AS NVARCHAR(4000))    AS description
7✔
57
            FROM sys.tables t
7✔
58
            LEFT JOIN sys.tables ht ON t.history_table_id = ht.object_id
7✔
59
            OUTER APPLY (
7✔
60
                SELECT
7✔
61
                    MAX(CASE WHEN c.generated_always_type = 1 THEN c.name END) AS start_column_name,
7✔
62
                    MAX(CASE WHEN c.generated_always_type = 2 THEN c.name END) AS end_column_name
7✔
63
                FROM sys.columns c
7✔
64
                WHERE c.object_id = t.object_id
7✔
65
                  AND c.generated_always_type IN (1, 2)
7✔
66
            ) pc
7✔
67
            LEFT JOIN sys.extended_properties ep
7✔
68
                ON ep.major_id = t.object_id AND ep.minor_id = 0
7✔
69
                AND ep.class = 1 AND ep.name = 'MS_Description'
7✔
70
            WHERE {tableFilter}
7✔
71
                AND t.temporal_type <> 1
7✔
72
            ORDER BY SCHEMA_NAME(t.schema_id), t.name
7✔
73
            """;
7✔
74

75
        using var cmd = connection.CreateCommand();
7✔
76
        cmd.CommandText = sql;
7✔
77

78
        using var reader = await cmd.ExecuteReaderAsync(SequentialResultBehavior, cancellationToken).ConfigureAwait(false);
7✔
79

80
        const int objectIdOrdinal = 0;
81
        const int schemaOrdinal = 1;
82
        const int nameOrdinal = 2;
83
        const int temporalOrdinal = 3;
84
        const int memoryOptimizedOrdinal = 4;
85
        const int fileTableOrdinal = 5;
86
        const int historySchemaOrdinal = 6;
87
        const int historyNameOrdinal = 7;
88
        const int periodStartOrdinal = 8;
89
        const int periodEndOrdinal = 9;
90
        const int descOrdinal = 10;
91

92
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
212✔
93
        {
94
            var objectId = reader.GetInt32(objectIdOrdinal);
205✔
95
            var schema = reader.GetString(schemaOrdinal);
205✔
96
            var name = reader.GetString(nameOrdinal);
205✔
97
            var temporalType = reader.GetByte(temporalOrdinal);
205✔
98
            var isMemoryOptimized = reader.GetBoolean(memoryOptimizedOrdinal);
205✔
99
            var isFileTable = reader.GetBoolean(fileTableOrdinal);
205✔
100
            var historySchema = reader.GetStringNull(historySchemaOrdinal);
205✔
101
            var historyName = reader.GetStringNull(historyNameOrdinal);
205✔
102
            var periodStartColumn = reader.GetStringNull(periodStartOrdinal);
205✔
103
            var periodEndColumn = reader.GetStringNull(periodEndOrdinal);
205✔
104
            var description = reader.GetStringNull(descOrdinal);
205✔
105

106
            SchemaQualifiedName? historyTableName = historySchema is not null && historyName is not null
205✔
107
                ? new SchemaQualifiedName { Schema = historySchema, Name = historyName }
205✔
108
                : null;
205✔
109

110
            var tableOptions = new TableOptions
205✔
111
            {
205✔
112
                IsTemporalTable = temporalType == 2,
205✔
113
                HistoryTable = historyTableName,
205✔
114
                PeriodStartColumnName = periodStartColumn,
205✔
115
                PeriodEndColumnName = periodEndColumn,
205✔
116
                IsMemoryOptimized = isMemoryOptimized,
205✔
117
                IsFileTable = isFileTable,
205✔
118
            };
205✔
119

120
            var tb = new TableBuilder()
205✔
121
                .WithQualifiedName(schema, name)
205✔
122
                .WithDescription(description)
205✔
123
                .WithOptions(tableOptions);
205✔
124

125
            // apply extended properties for the table itself (class=1, major_id=object_id, minor_id=0)
126
            ApplyExtendedProperties((1, objectId, 0), tb);
205✔
127

128
            tables[objectId] = tb;
205✔
129
        }
130

131
        return tables;
7✔
132
    }
7✔
133

134
    private async Task ReadTableColumnsAsync(
135
        SqlConnection connection,
136
        Dictionary<int, TableBuilder> tables,
137
        string tableFilter,
138
        CancellationToken cancellationToken)
139
    {
140
        var sql = $"""
7✔
141
            SELECT
7✔
142
                c.object_id,
7✔
143
                c.column_id,
7✔
144
                c.name                              AS column_name,
7✔
145
                COALESCE
7✔
146
                (
7✔
147
                    CASE WHEN ut.is_assembly_type = 1 AND ut.is_user_defined = 0 THEN ut.name END,
7✔
148
                    st.name,
7✔
149
                    ut.name
7✔
150
                )                                   AS system_type_name,
7✔
151
                ut.name                             AS user_type_name,
7✔
152
                c.max_length,
7✔
153
                c.precision,
7✔
154
                c.scale,
7✔
155
                c.is_nullable,
7✔
156
                c.is_identity,
7✔
157
                c.is_computed,
7✔
158
                CAST(CASE WHEN c.system_type_id = 189 THEN 1 ELSE 0 END AS BIT) AS is_rowversion,
7✔
159
                c.collation_name,
7✔
160
                CAST(ic.seed_value AS BIGINT)       AS identity_seed,
7✔
161
                CAST(ic.increment_value AS BIGINT)  AS identity_increment,
7✔
162
                cc.definition                       AS computed_sql,
7✔
163
                cc.is_persisted,
7✔
164
                dc.definition                       AS default_sql,
7✔
165
                CAST(ep.value AS NVARCHAR(4000))    AS description
7✔
166
            FROM sys.columns c
7✔
167
            INNER JOIN sys.tables t ON c.object_id = t.object_id
7✔
168
            INNER JOIN sys.types ut ON c.user_type_id = ut.user_type_id
7✔
169
            LEFT JOIN sys.types st
7✔
170
                ON c.system_type_id = st.system_type_id
7✔
171
                AND st.system_type_id = st.user_type_id
7✔
172
            LEFT JOIN sys.identity_columns ic
7✔
173
                ON c.object_id = ic.object_id AND c.column_id = ic.column_id
7✔
174
            LEFT JOIN sys.computed_columns cc
7✔
175
                ON c.object_id = cc.object_id AND c.column_id = cc.column_id
7✔
176
            LEFT JOIN sys.default_constraints dc
7✔
177
                ON c.default_object_id = dc.object_id
7✔
178
            LEFT JOIN sys.extended_properties ep
7✔
179
                ON ep.major_id = c.object_id AND ep.minor_id = c.column_id
7✔
180
                AND ep.class = 1 AND ep.name = 'MS_Description'
7✔
181
            WHERE {tableFilter}
7✔
182
            ORDER BY c.object_id, c.column_id
7✔
183
            """;
7✔
184

185
        using var cmd = connection.CreateCommand();
7✔
186
        cmd.CommandText = sql;
7✔
187

188
        using var reader = await cmd.ExecuteReaderAsync(SequentialResultBehavior, cancellationToken).ConfigureAwait(false);
7✔
189

190
        const int objectIdOrdinal = 0;
191
        const int columnIdOrdinal = 1;
192
        const int columnNameOrdinal = 2;
193
        const int systemTypeOrdinal = 3;
194
        const int userTypeOrdinal = 4;
195
        const int maxLengthOrdinal = 5;
196
        const int precisionOrdinal = 6;
197
        const int scaleOrdinal = 7;
198
        const int nullableOrdinal = 8;
199
        const int identityOrdinal = 9;
200
        const int computedOrdinal = 10;
201
        const int rowVersionOrdinal = 11;
202
        const int collationOrdinal = 12;
203
        const int seedOrdinal = 13;
204
        const int incrementOrdinal = 14;
205
        const int computedSqlOrdinal = 15;
206
        const int persistedOrdinal = 16;
207
        const int defaultSqlOrdinal = 17;
208
        const int descriptionOrdinal = 18;
209

210
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
1,643✔
211
        {
212
            var objectId = reader.GetInt32(objectIdOrdinal);
1,636✔
213
            if (!tables.TryGetValue(objectId, out var tableBuilder))
1,636✔
214
                continue;
215

216
            var columnId = reader.GetInt32(columnIdOrdinal);
1,612✔
217
            var columnName = reader.GetString(columnNameOrdinal);
1,612✔
218
            var systemTypeName = reader.GetString(systemTypeOrdinal);
1,612✔
219
            var userTypeName = reader.GetStringNull(userTypeOrdinal) ?? systemTypeName;
1,612!
220
            var maxLength = reader.GetInt16(maxLengthOrdinal);
1,612✔
221
            var precision = reader.GetByte(precisionOrdinal);
1,612✔
222
            var scale = reader.GetByte(scaleOrdinal);
1,612✔
223
            var isNullable = reader.GetBoolean(nullableOrdinal);
1,612✔
224
            var isIdentity = reader.GetBoolean(identityOrdinal);
1,612✔
225
            var isComputed = reader.GetBoolean(computedOrdinal);
1,612✔
226
            var isRowVersion = reader.GetBoolean(rowVersionOrdinal);
1,612✔
227
            var collation = reader.GetStringNull(collationOrdinal);
1,612✔
228
            var identitySeed = reader.GetInt64Null(seedOrdinal);
1,612✔
229
            var identityIncrement = reader.GetInt64Null(incrementOrdinal);
1,612✔
230
            var computedSql = reader.GetStringNull(computedSqlOrdinal);
1,612✔
231
            var isPersisted = reader.GetBooleanNull(persistedOrdinal) ?? false;
1,612✔
232
            var defaultSql = reader.GetStringNull(defaultSqlOrdinal);
1,612✔
233
            var description = reader.GetStringNull(descriptionOrdinal);
1,612✔
234

235
            // Map SQL Server system type to DbType and CLR type, and determine Unicode/fixed-length attributes
236
            var (dbType, sqlDbType, systemType, isUnicode, isFixedLength) = SqlServerTypeMapper.MapNativeType(systemTypeName);
1,612✔
237

238
            // Format the native type name with length/precision/scale as appropriate for the type
239
            var nativeTypeName = FormatNativeTypeName(systemTypeName, userTypeName, maxLength, precision, scale);
1,612✔
240

241
            // Normalize max length for character types (e.g. -1 for MAX) and binary types, and set to null for types where it doesn't apply
242
            var maxLengthValue = NormalizeMaxLength(systemTypeName, maxLength);
1,612✔
243

244
            // Only set precision and scale for types where they apply (e.g. decimal, numeric, time, datetime2); set to null for other types
245
            byte? precisionValue = HasPrecision(systemTypeName) ? precision : null;
1,612✔
246

247
            // Scale is applicable for decimal/numeric, and also for time/datetime2 (where it represents fractional seconds precision). For other types, set to null.
248
            var scaleValue = HasScale(systemTypeName) ? (int?)scale : null;
1,612✔
249

250
            tableBuilder.AddColumn(columnBuilder =>
1,612✔
251
            {
1,612✔
252
                columnBuilder
1,612✔
253
                    .WithName(columnName)
1,612✔
254
                    .WithOrdinalPosition(columnId)
1,612✔
255
                    .WithIsNullable(isNullable)
1,612✔
256
                    .WithDefaultValueSql(defaultSql)
1,612✔
257
                    .WithIsIdentity(isIdentity)
1,612✔
258
                    .WithIdentitySeed(identitySeed)
1,612✔
259
                    .WithIdentityIncrement(identityIncrement)
1,612✔
260
                    .WithIsComputed(isComputed)
1,612✔
261
                    .WithComputedColumnSql(computedSql)
1,612✔
262
                    .WithIsStored(isPersisted)
1,612✔
263
                    .WithIsRowVersion(isRowVersion)
1,612✔
264
                    .WithIsConcurrencyToken(isRowVersion)
1,612✔
265
                    .WithCollation(collation)
1,612✔
266
                    .WithDescription(description)
1,612✔
267
                    .WithNativeTypeName(nativeTypeName)
1,612✔
268
                    .WithDbType(dbType)
1,612✔
269
                    .WithSystemType(systemType)
1,612✔
270
                    .WithMaxLength(maxLengthValue)
1,612✔
271
                    .WithPrecision(precisionValue)
1,612✔
272
                    .WithScale(scaleValue)
1,612✔
273
                    .WithIsUnicode(isUnicode)
1,612✔
274
                    .WithIsFixedLength(isFixedLength);
1,612✔
275

1,612✔
276
                // Apply extended properties for the column (class=1, major_id=object_id, minor_id=column_id)
1,612✔
277
                ApplyExtendedProperties((1, objectId, columnId), columnBuilder);
1,612✔
278
                columnBuilder.WithAnnotation(SqlServerAnnotations.SqlDbType, sqlDbType.ToString());
1,612✔
279
            });
1,612✔
280
        }
281
    }
7✔
282

283
    private async Task ReadKeyConstraintsAsync(
284
        SqlConnection connection,
285
        Dictionary<int, TableBuilder> tables,
286
        CancellationToken cancellationToken)
287
    {
288
        // Key constraints (primary keys and unique constraints) are read together since they share underlying index metadata.
289

290
        // Dictionary to accumulate constraint info, keyed by (object_id, constraint_name). Value is (constraint_type, is_clustered, list of columns in order).
291
        var constraints = new Dictionary<(int ObjectId, string Name), (string Type, bool IsClustered, List<ColumnReference> Columns)>();
7✔
292

293
        const string sql = """
294
            SELECT
295
                kc.parent_object_id,
296
                kc.name             AS constraint_name,
297
                kc.type             AS constraint_type,
298
                i.type              AS index_type,
299
                c.name              AS column_name,
300
                ic.is_descending_key
301
            FROM sys.key_constraints kc
302
            INNER JOIN sys.indexes i
303
                ON kc.parent_object_id = i.object_id AND kc.unique_index_id = i.index_id
304
            INNER JOIN sys.index_columns ic
305
                ON i.object_id = ic.object_id AND i.index_id = ic.index_id
306
            INNER JOIN sys.columns c
307
                ON ic.object_id = c.object_id AND ic.column_id = c.column_id
308
            INNER JOIN sys.tables t ON kc.parent_object_id = t.object_id
309
            WHERE ic.is_included_column = 0
310
              AND t.is_ms_shipped = 0
311
            ORDER BY kc.parent_object_id, kc.name, ic.key_ordinal
312
            """;
313

314
        using var cmd = connection.CreateCommand();
7✔
315
        cmd.CommandText = sql;
7✔
316

317
        using var reader = await cmd.ExecuteReaderAsync(SequentialResultBehavior, cancellationToken).ConfigureAwait(false);
7✔
318

319
        const int parentOrdinal = 0;
320
        const int nameOrdinal = 1;
321
        const int typeOrdinal = 2;
322
        const int indexTypeOrdinal = 3;
323
        const int columnNameOrdinal = 4;
324
        const int descendOrdinal = 5;
325

326
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
259✔
327
        {
328
            // Filter rows based on object_id to avoid processing constraints for tables we're not including.
329
            // This is more efficient than filtering in-memory after reading all constraints.
330
            var objectId = reader.GetInt32(parentOrdinal);
252✔
331

332
            if (!tables.ContainsKey(objectId))
252✔
333
                continue;
334

335
            var constraintName = reader.GetString(nameOrdinal);
217✔
336
            var type = reader.GetString(typeOrdinal).Trim();
217✔
337
            var indexType = reader.GetByte(indexTypeOrdinal);
217✔
338
            var columnName = reader.GetString(columnNameOrdinal);
217✔
339
            var sortDirection = reader.GetBoolean(descendOrdinal)
217!
340
                ? SortDirection.Descending
217✔
341
                : SortDirection.Ascending;
217✔
342

343
            var key = (objectId, constraintName);
217✔
344

345
            // If we haven't seen this constraint before, create a new entry in the dictionary with its type and clustering info.
346
            // Otherwise, we'll just add columns to the existing entry.
347
            if (!constraints.TryGetValue(key, out var kc))
217✔
348
            {
349
                kc = (type, indexType == 1, []);
193✔
350

351
                constraints[key] = kc;
193✔
352
            }
353

354
            ColumnReference reference = new()
217✔
355
            {
217✔
356
                ColumnName = columnName,
217✔
357
                SortDirection = sortDirection,
217✔
358
            };
217✔
359
            kc.Columns.Add(reference);
217✔
360
        }
361

362
        // Now that we've read all the constraints and their columns, we can apply them to the corresponding tables.
363
        foreach (var ((objectId, name), (type, isClustered, columns)) in constraints)
400✔
364
        {
365
            var tableBuilder = tables[objectId];
193✔
366
            var columnReferences = columns.ToArray();
193✔
367

368
            // SQL Server represents both primary keys and unique constraints in the sys.key_constraints view,
369
            // distinguished by the 'type' column ('PK' for primary key, 'UQ' for unique constraint).
370
            // We need to check the type to determine whether to call WithPrimaryKey or AddUniqueConstraint on the TableBuilder.
371

372
            if (type == "PK")
193!
373
                tableBuilder.WithPrimaryKey(name, isClustered, columnReferences);
193✔
374
            else
375
                tableBuilder.AddUniqueConstraint(name, columnReferences);
×
376
        }
377
    }
7✔
378

379
    private async Task ReadTableIndexesAsync(
380
        SqlConnection connection,
381
        Dictionary<int, TableBuilder> tables,
382
        CancellationToken cancellationToken)
383
    {
384
        // Dictionary to accumulate index info, keyed by (object_id, index_id).
385
        var indexes = new Dictionary<(int ObjectId, int IndexId), IndexBuilder>();
7✔
386

387
        const string sql = """
388
            SELECT
389
                i.object_id,
390
                i.index_id,
391
                i.name              AS index_name,
392
                i.is_unique,
393
                i.type              AS index_type,
394
                i.is_disabled,
395
                i.has_filter,
396
                i.filter_definition,
397
                i.fill_factor,
398
                c.name              AS column_name,
399
                ic.is_descending_key,
400
                ic.is_included_column
401
            FROM sys.indexes i
402
            INNER JOIN sys.index_columns ic
403
                ON i.object_id = ic.object_id AND i.index_id = ic.index_id
404
            INNER JOIN sys.columns c
405
                ON ic.object_id = c.object_id AND ic.column_id = c.column_id
406
            INNER JOIN sys.tables t ON i.object_id = t.object_id
407
            WHERE t.is_ms_shipped = 0
408
              AND i.type > 0
409
              AND i.is_primary_key = 0
410
              AND i.is_unique_constraint = 0
411
              AND i.name IS NOT NULL
412
            ORDER BY i.object_id, i.index_id, ic.key_ordinal, ic.index_column_id
413
            """;
414

415
        using var cmd = connection.CreateCommand();
7✔
416
        cmd.CommandText = sql;
7✔
417

418
        using var reader = await cmd.ExecuteReaderAsync(SequentialResultBehavior, cancellationToken).ConfigureAwait(false);
7✔
419

420
        const int objectIdOrdinal = 0;
421
        const int indexIdOrdinal = 1;
422
        const int nameOrdinal = 2;
423
        const int uniqueOrdinal = 3;
424
        const int typeOrdinal = 4;
425
        const int disabledOrdinal = 5;
426
        const int filterOrdinal = 6;
427
        const int filterDefOrdinal = 7;
428
        const int fillFactorOrdinal = 8;
429
        const int colNameOrdinal = 9;
430
        const int descendOrdinal = 10;
431
        const int includedOrdinal = 11;
432

433
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
112✔
434
        {
435
            var objectId = reader.GetInt32(objectIdOrdinal);
105✔
436

437
            // Filter rows based on object_id to avoid processing indexes for tables we're not including.
438
            if (!tables.ContainsKey(objectId))
105✔
439
                continue;
440

441
            var indexId = reader.GetInt32(indexIdOrdinal);
78✔
442
            var indexName = reader.GetString(nameOrdinal);
78✔
443
            var isUnique = reader.GetBoolean(uniqueOrdinal);
78✔
444
            var indexType = reader.GetByte(typeOrdinal);
78✔
445
            var isDisabled = reader.GetBoolean(disabledOrdinal);
78✔
446
            var hasFilter = reader.GetBoolean(filterOrdinal);
78✔
447
            var filterExpression = reader.GetStringNull(filterDefOrdinal);
78✔
448
            var fillFactor = reader.GetByte(fillFactorOrdinal);
78✔
449
            var columnName = reader.GetString(colNameOrdinal);
78✔
450
            var isDescending = reader.GetBoolean(descendOrdinal);
78✔
451
            var isIncluded = reader.GetBoolean(includedOrdinal);
78✔
452

453
            var key = (objectId, indexId);
78✔
454

455
            // If we haven't seen this index before, create a new IndexBuilder and add it to the dictionary.
456
            // Otherwise, we'll just add columns to the existing builder.
457
            if (!indexes.TryGetValue(key, out var indexBuilder))
78✔
458
            {
459
                indexBuilder = new IndexBuilder()
60!
460
                    .WithName(indexName)
60✔
461
                    .WithIsUnique(isUnique)
60✔
462
                    .WithIsClustered(indexType == 1)
60✔
463
                    .WithFillFactor(fillFactor == 0 ? null : fillFactor)
60✔
464
                    .WithIsDisabled(isDisabled);
60✔
465

466
                // Apply extended properties for the index (class=7, major_id=object_id, minor_id=index_id)
467
                ApplyExtendedProperties((7, objectId, indexId), indexBuilder);
60✔
468

469
                // If the index has a filter, set the IsFiltered property and the FilterExpression if it's not null.
470
                if (hasFilter)
60!
471
                {
472
                    indexBuilder.WithIsFiltered(true);
×
473
                    if (filterExpression is not null)
×
474
                        indexBuilder.WithFilterExpression(filterExpression);
×
475
                }
476

477
                // SQL Server supports different index types: 1 = clustered, 2 = nonclustered, 3 = XML, 4 = spatial, 5 = columnstore, 6 = columnstore_clustered, 7 = hash.
478
                // We can map these to the IndexType property on the IndexBuilder.
479
                if (indexType is 5 or 6)
60!
480
                    indexBuilder.WithIndexType("COLUMNSTORE");
×
481
                else if (indexType == 7)
60!
482
                    indexBuilder.WithIndexType("HASH");
×
483

484
                indexes[key] = indexBuilder;
60✔
485
            }
486

487
            // Included columns are part of the index but not key columns, so they don't have sort direction.
488
            // We need to call AddIncludedColumn instead of AddColumn for these.
489
            if (isIncluded)
78!
490
            {
491
                indexBuilder.AddIncludedColumn(columnName);
×
492
            }
493
            else
494
            {
495
                var sortDirection = isDescending ? SortDirection.Descending : SortDirection.Ascending;
78!
496
                indexBuilder.AddColumn(columnName, sortDirection);
78✔
497
            }
498
        }
499

500
        // Now that we've read all the indexes and their columns, we can apply them to the corresponding tables.
501
        foreach (var ((objectId, _), indexBuilder) in indexes)
134✔
502
            tables[objectId].AddIndex(indexBuilder.Build());
60✔
503
    }
7✔
504

505
    private static async Task ReadCheckConstraintsAsync(
506
        SqlConnection connection,
507
        Dictionary<int, TableBuilder> tables,
508
        CancellationToken cancellationToken)
509
    {
510
        const string sql = """
511
            SELECT
512
                cc.parent_object_id,
513
                cc.name         AS constraint_name,
514
                cc.definition
515
            FROM sys.check_constraints cc
516
            INNER JOIN sys.tables t ON cc.parent_object_id = t.object_id
517
            WHERE t.is_ms_shipped = 0
518
            ORDER BY cc.parent_object_id, cc.name
519
            """;
520

521
        using var cmd = connection.CreateCommand();
7✔
522
        cmd.CommandText = sql;
7✔
523

524
        using var reader = await cmd.ExecuteReaderAsync(SequentialResultBehavior, cancellationToken).ConfigureAwait(false);
7✔
525

526
        const int parentOrdinal = 0;
527
        const int nameOrdinal = 1;
528
        const int defOrdinal = 2;
529

530
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
7!
531
        {
532
            var objectId = reader.GetInt32(parentOrdinal);
×
533

534
            // Filter rows based on object_id to avoid processing constraints for tables we're not including.
535
            if (!tables.TryGetValue(objectId, out var tableBuilder))
×
536
                continue;
537

538
            var name = reader.GetString(nameOrdinal);
×
539
            var definition = reader.GetString(defOrdinal);
×
540

541
            tableBuilder.AddCheckConstraint(name, definition);
×
542
        }
543
    }
7✔
544

545
    private static async Task ReadTableForeignKeysAsync(
546
        SqlConnection connection,
547
        Dictionary<int, TableBuilder> tables,
548
        CancellationToken cancellationToken)
549
    {
550
        // Dictionary to accumulate foreign key info, keyed by (parent_object_id, fk_name).
551
        var foreignKeys = new Dictionary<(int ObjectId, string Name), ForeignKeyBuilder>();
7✔
552

553
        const string sql = """
554
            SELECT
555
                fk.parent_object_id,
556
                fk.name                         AS fk_name,
557
                SCHEMA_NAME(rt.schema_id)       AS principal_schema,
558
                rt.name                         AS principal_table,
559
                fk.delete_referential_action,
560
                fk.update_referential_action,
561
                fk.is_disabled,
562
                pc.name                         AS parent_column,
563
                rc.name                         AS referenced_column
564
            FROM sys.foreign_keys fk
565
            INNER JOIN sys.foreign_key_columns fkc
566
                ON fk.object_id = fkc.constraint_object_id
567
            INNER JOIN sys.tables t ON fk.parent_object_id = t.object_id
568
            INNER JOIN sys.tables rt ON fk.referenced_object_id = rt.object_id
569
            INNER JOIN sys.columns pc
570
                ON fkc.parent_object_id = pc.object_id AND fkc.parent_column_id = pc.column_id
571
            INNER JOIN sys.columns rc
572
                ON fkc.referenced_object_id = rc.object_id AND fkc.referenced_column_id = rc.column_id
573
            WHERE t.is_ms_shipped = 0
574
            ORDER BY fk.parent_object_id, fk.name, fkc.constraint_column_id
575
            """;
576

577
        using var cmd = connection.CreateCommand();
7✔
578
        cmd.CommandText = sql;
7✔
579

580
        using var reader = await cmd.ExecuteReaderAsync(SequentialResultBehavior, cancellationToken).ConfigureAwait(false);
7✔
581

582
        const int parentOrdinal = 0;
583
        const int nameOrdinal = 1;
584
        const int pSchemaOrdinal = 2;
585
        const int pTableOrdinal = 3;
586
        const int deleteOrdinal = 4;
587
        const int updateOrdinal = 5;
588
        const int disabledOrdinal = 6;
589
        const int parentColOrdinal = 7;
590
        const int refColOrdinal = 8;
591

592
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
147✔
593
        {
594
            var objectId = reader.GetInt32(parentOrdinal);
140✔
595

596
            // Filter rows based on parent_object_id to avoid processing foreign keys for tables we're not including.
597
            if (!tables.ContainsKey(objectId))
140✔
598
                continue;
599

600
            var fkName = reader.GetString(nameOrdinal);
120✔
601
            var principalSchema = reader.GetString(pSchemaOrdinal);
120✔
602
            var principalTable = reader.GetString(pTableOrdinal);
120✔
603
            var onDelete = MapReferentialAction(reader.GetByte(deleteOrdinal));
120✔
604
            var onUpdate = MapReferentialAction(reader.GetByte(updateOrdinal));
120✔
605
            var isDisabled = reader.GetBoolean(disabledOrdinal);
120✔
606
            var parentColumn = reader.GetString(parentColOrdinal);
120✔
607
            var referencedColumn = reader.GetString(refColOrdinal);
120✔
608

609
            var key = (objectId, fkName);
120✔
610

611
            // If we haven't seen this foreign key before, create a new ForeignKeyBuilder and add it to the dictionary.
612
            if (!foreignKeys.TryGetValue(key, out var foreignKeyBuilder))
120✔
613
            {
614
                foreignKeyBuilder = new ForeignKeyBuilder()
114✔
615
                    .WithName(fkName)
114✔
616
                    .WithPrincipalTableName(principalSchema, principalTable)
114✔
617
                    .WithOnDelete(onDelete)
114✔
618
                    .WithOnUpdate(onUpdate)
114✔
619
                    .WithIsDisabled(isDisabled);
114✔
620

621
                foreignKeys[key] = foreignKeyBuilder;
114✔
622
            }
623

624
            // Add the column mapping to the ForeignKeyBuilder. Since the query is ordered by constraint_column_id, the columns will be added in the correct order.
625
            foreignKeyBuilder.AddColumnMapping(parentColumn, referencedColumn);
120✔
626
        }
627

628
        // Now that we've read all the foreign keys and their column mappings, we can apply them to the corresponding tables.
629
        foreach (var ((objectId, _), foreignKeyBuilder) in foreignKeys)
242✔
630
            tables[objectId].AddForeignKey(foreignKeyBuilder.Build());
114✔
631
    }
7✔
632

633
    private static async Task ReadTableTriggersAsync(
634
        SqlConnection connection,
635
        Dictionary<int, TableBuilder> tables,
636
        CancellationToken cancellationToken)
637
    {
638
        // Dictionary to accumulate trigger info, keyed by (parent_object_id, trigger_name).
639
        var triggerData = new Dictionary<(int ObjectId, string Name), (bool IsDisabled, bool IsInsteadOf, string? Definition, TriggerEvent Events)>();
7✔
640

641
        const string sql = """
642
            SELECT
643
                tr.parent_id,
644
                tr.name                     AS trigger_name,
645
                tr.is_disabled,
646
                tr.is_instead_of_trigger,
647
                m.definition,
648
                te.type_desc                AS event_type
649
            FROM sys.triggers tr
650
            INNER JOIN sys.trigger_events te ON tr.object_id = te.object_id
651
            LEFT JOIN sys.sql_modules m ON tr.object_id = m.object_id
652
            WHERE tr.parent_class = 1
653
            ORDER BY tr.parent_id, tr.name
654
            """;
655

656
        using var cmd = connection.CreateCommand();
7✔
657
        cmd.CommandText = sql;
7✔
658

659
        using var reader = await cmd.ExecuteReaderAsync(SequentialResultBehavior, cancellationToken).ConfigureAwait(false);
7✔
660

661
        const int parentOrdinal = 0;
662
        const int nameOrdinal = 1;
663
        const int disabledOrdinal = 2;
664
        const int insteadOrdinal = 3;
665
        const int definitionOrdinal = 4;
666
        const int eventOrdinal = 5;
667

668
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
7!
669
        {
670
            var parentId = reader.GetInt32(parentOrdinal);
×
671

672
            if (!tables.ContainsKey(parentId))
×
673
                continue;
674

675
            var name = reader.GetString(nameOrdinal);
×
676
            var isDisabled = reader.GetBoolean(disabledOrdinal);
×
677
            var isInsteadOf = reader.GetBoolean(insteadOrdinal);
×
678
            var definition = reader.GetStringNull(definitionOrdinal);
×
679
            var eventType = reader.GetString(eventOrdinal);
×
680

681
            var triggerEvent = MapTriggerEvent(eventType);
×
682

683
            var key = (parentId, name);
×
684

685
            // If we haven't seen this trigger before, create a new entry in the dictionary with its disabled/instead_of/definition info.
686
            if (!triggerData.TryGetValue(key, out var td))
×
687
            {
688
                triggerData[key] = (isDisabled, isInsteadOf, definition, triggerEvent);
×
689
            }
690
            else
691
            {
692
                td.Events |= triggerEvent;
×
693
                triggerData[key] = td;
×
694
            }
695
        }
696

697
        // Now that we've read all the triggers and their events, we can apply them to the corresponding tables.
698
        foreach (var ((parentId, name), (isDisabled, isInsteadOf, definition, triggerEvents)) in triggerData)
14!
699
        {
700
            // Determine the trigger timing based on the is_instead_of_trigger column.
701
            // If it's an INSTEAD OF trigger, the timing is InsteadOf; otherwise, it's After (SQL Server doesn't have BEFORE triggers).
702
            var timing = isInsteadOf ? TriggerTiming.InsteadOf : TriggerTiming.After;
×
703
            var trigger = new Trigger
×
704
            {
×
705
                Name = name,
×
706
                Timing = timing,
×
707
                Events = triggerEvents,
×
708
                Definition = definition,
×
709
                IsDisabled = isDisabled,
×
710
            };
×
711

712
            tables[parentId].AddTrigger(trigger);
×
713
        }
714
    }
7✔
715

716
    private static TriggerEvent MapTriggerEvent(string eventType) => eventType switch
×
717
    {
×
718
        "INSERT" => TriggerEvent.Insert,
×
719
        "UPDATE" => TriggerEvent.Update,
×
720
        "DELETE" => TriggerEvent.Delete,
×
721
        _ => TriggerEvent.None,
×
722
    };
×
723
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc