• 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

81.78
/src/SchemaSaurus.SqlServer/SqlServerSchemaReader.cs
1
using System.Data;
2
using System.Globalization;
3

4
using Microsoft.Data.SqlClient;
5

6
using SchemaSaurus.Metadata;
7
using SchemaSaurus.Metadata.Builders;
8
using SchemaSaurus.Metadata.Extensions;
9
using SchemaSaurus.Metadata.Provider;
10

11
namespace SchemaSaurus.SqlServer;
12

13
/// <summary>
14
/// Reads structural metadata from a SQL Server database using <c>sys.*</c> catalog views.
15
/// Schema/table filtering is pushed into SQL WHERE clauses. Extended properties (MS_Description)
16
/// are joined inline. Large read methods are decomposed into focused private sub-methods.
17
/// </summary>
18
public sealed partial class SqlServerSchemaReader : DatabaseSchemaReader<SqlConnection>
19
{
20
    private const CommandBehavior SequentialResultBehavior = CommandBehavior.SingleResult | CommandBehavior.SequentialAccess;
21

22
    private readonly Dictionary<(int Class, int MajorId, int MinorId), List<KeyValuePair<string, object?>>> _extendedProperties = [];
7✔
23

24
    /// <inheritdoc />
25
    public override string ProviderName => "SqlServer";
7✔
26

27
    /// <inheritdoc />
28
    protected override async Task ReadDatabaseMetadataAsync(
29
        SqlConnection connection,
30
        DatabaseModelBuilder builder,
31
        CancellationToken cancellationToken)
32
    {
33
        // Read extended properties first so that they can be applied to the relevant metadata elements as we read them,
34
        // without needing to do lookups back into the database later.
35
        await ReadExtendedPropertiesAsync(connection, cancellationToken).ConfigureAwait(false);
7✔
36

37
        const string sql = """
38
            SELECT
39
                CAST(SERVERPROPERTY('Collation') AS NVARCHAR(256))  AS collation,
40
                SCHEMA_NAME()                                       AS default_schema,
41
                @@VERSION                                           AS server_version,
42
                CAST(SERVERPROPERTY('Edition') AS NVARCHAR(256))    AS edition,
43
                CAST(SERVERPROPERTY('EngineEdition') AS INT)        AS engine_edition,
44
                (SELECT compatibility_level
45
                 FROM sys.databases
46
                 WHERE name = DB_NAME())                            AS compat_level
47
            """;
48

49
        using var cmd = connection.CreateCommand();
7✔
50
        cmd.CommandText = sql;
7✔
51

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

54
        if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
7!
55
            return;
×
56

57
        const int collationOrdinal = 0;
58
        const int schemaOrdinal = 1;
59
        const int versionOrdinal = 2;
60
        const int editionOrdinal = 3;
61
        const int engineOrdinal = 4;
62
        const int compatOrdinal = 5;
63

64
        var collation = reader.GetStringNull(collationOrdinal);
7✔
65
        var defaultSchema = reader.GetStringNull(schemaOrdinal);
7✔
66
        var serverVersion = reader.GetStringNull(versionOrdinal);
7✔
67
        var edition = reader.GetStringNull(editionOrdinal);
7✔
68
        var engineEditionValue = reader.GetInt32Null(engineOrdinal);
7✔
69
        var compatibilityLevelValue = reader.GetByteNull(compatOrdinal);
7✔
70

71
        var engineEdition = engineEditionValue is null ? null : GetEngineEditionName(engineEditionValue.Value);
7!
72
        var compatibilityLevel = compatibilityLevelValue?.ToString(CultureInfo.InvariantCulture);
7!
73

74
        builder
7✔
75
            .WithCollation(collation)
7✔
76
            .WithDefaultSchemaName(defaultSchema)
7✔
77
            .WithServerVersion(serverVersion)
7✔
78
            .WithEdition(edition)
7✔
79
            .WithEngineEdition(engineEdition)
7✔
80
            .WithCompatibilityLevel(compatibilityLevel);
7✔
81

82
        // Apply extended properties to the database itself (class=0, major_id=0, minor_id=0).
83
        ApplyExtendedProperties((0, 0, 0), builder);
7✔
84
    }
7✔
85

86

87
    private async Task ReadExtendedPropertiesAsync(
88
        SqlConnection connection,
89
        CancellationToken cancellationToken)
90
    {
91
        _extendedProperties.Clear();
7✔
92

93
        const string sql = """
94
            SELECT
95
                ep.class,
96
                ep.major_id,
97
                ep.minor_id,
98
                ep.name,
99
                ep.value
100
            FROM sys.extended_properties ep
101
            ORDER BY ep.class, ep.major_id, ep.minor_id, ep.name
102
            """;
103

104
        using var cmd = connection.CreateCommand();
7✔
105
        cmd.CommandText = sql;
7✔
106

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

109
        const int classOrdinal = 0;
110
        const int majorIdOrdinal = 1;
111
        const int minorIdOrdinal = 2;
112
        const int nameOrdinal = 3;
113
        const int valueOrdinal = 4;
114

115
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
56✔
116
        {
117
            var classId = reader.GetByte(classOrdinal);
49✔
118
            var majorId = reader.GetInt32(majorIdOrdinal);
49✔
119
            var minorId = reader.GetInt32(minorIdOrdinal);
49✔
120

121
            var name = reader.GetString(nameOrdinal);
49✔
122
            var value = reader.GetValueNull(valueOrdinal);
49✔
123

124
            var key = (classId, majorId, minorId);
49✔
125
            if (!_extendedProperties.TryGetValue(key, out var values))
49✔
126
            {
127
                values = [];
35✔
128
                _extendedProperties[key] = values;
35✔
129
            }
130

131
            values.Add(new KeyValuePair<string, object?>(name, value));
49✔
132
        }
133
    }
7✔
134

135
    private async Task ReadParametersAsync<TBuilder>(
136
        SqlConnection connection,
137
        Dictionary<int, TBuilder> builders,
138
        string objectTypeFilter,
139
        CancellationToken cancellationToken)
140
        where TBuilder : class
141
    {
142
        var sql = $"""
6✔
143
            SELECT
6✔
144
                par.object_id,
6✔
145
                par.name                        AS param_name,
6✔
146
                par.parameter_id,
6✔
147
                st.name                         AS system_type_name,
6✔
148
                TYPE_NAME(par.user_type_id)     AS user_type_name,
6✔
149
                par.max_length,
6✔
150
                par.precision,
6✔
151
                par.scale,
6✔
152
                par.is_output
6✔
153
            FROM sys.parameters par
6✔
154
            INNER JOIN sys.objects o ON par.object_id = o.object_id
6✔
155
            INNER JOIN sys.types st
6✔
156
                ON par.system_type_id = st.system_type_id
6✔
157
                AND st.system_type_id = st.user_type_id
6✔
158
            WHERE o.is_ms_shipped = 0 AND par.parameter_id > 0 AND {objectTypeFilter}
6✔
159
            ORDER BY par.object_id, par.parameter_id
6✔
160
            """;
6✔
161

162
        using var cmd = connection.CreateCommand();
6✔
163
        cmd.CommandText = sql;
6✔
164

165
        using var reader = await cmd.ExecuteReaderAsync(SequentialResultBehavior, cancellationToken).ConfigureAwait(false);
6✔
166

167
        const int objectIdOrdinal = 0;
168
        const int nameOrdinal = 1;
169
        const int paramIdOrdinal = 2;
170
        const int sysTypeOrdinal = 3;
171
        const int userTypeOrdinal = 4;
172
        const int maxLenOrdinal = 5;
173
        const int precisionOrdinal = 6;
174
        const int scaleOrdinal = 7;
175
        const int outputOrdinal = 8;
176

177
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
78✔
178
        {
179
            var objectId = reader.GetInt32(objectIdOrdinal);
72✔
180

181
            if (!builders.TryGetValue(objectId, out var b))
72✔
182
                continue;
183

184
            var paramName = reader.GetString(nameOrdinal);
72✔
185
            var paramOrdinal = reader.GetInt32(paramIdOrdinal);
72✔
186
            var systemTypeName = reader.GetString(sysTypeOrdinal);
72✔
187
            var userTypeName = reader.GetStringNull(userTypeOrdinal) ?? systemTypeName;
72!
188
            var maxLength = reader.GetInt16(maxLenOrdinal);
72✔
189
            var precision = reader.GetByte(precisionOrdinal);
72✔
190
            var scale = reader.GetByte(scaleOrdinal);
72✔
191
            var isOutput = reader.GetBoolean(outputOrdinal);
72✔
192

193
            var maxLengthValue = NormalizeMaxLength(systemTypeName, maxLength);
72✔
194
            byte? precisionValue = HasPrecision(systemTypeName) ? precision : null;
72!
195
            var scaleValue = HasScale(systemTypeName) ? (int?)scale : null;
72✔
196

197
            var (dbType, sqlDbType, systemType, isUnicode, isFixedLength) = SqlServerTypeMapper.MapNativeType(systemTypeName);
72✔
198
            var nativeTypeName = FormatNativeTypeName(systemTypeName, userTypeName, maxLength, precision, scale);
72✔
199

200
            var direction = isOutput ? Metadata.ParameterDirection.Output : Metadata.ParameterDirection.Input;
72✔
201

202
            void Configure(ParameterBuilder parameterBuilder)
203
            {
204
                parameterBuilder
205
                    .WithName(paramName)
206
                    .WithOrdinal(paramOrdinal)
207
                    .WithDirection(direction)
208
                    .WithNativeTypeName(nativeTypeName)
209
                    .WithDbType(dbType)
210
                    .WithSystemType(systemType)
211
                    .WithMaxLength(maxLengthValue)
212
                    .WithPrecision(precisionValue)
213
                    .WithScale(scaleValue)
214
                    .WithIsUnicode(isUnicode)
215
                    .WithIsFixedLength(isFixedLength);
216

217
                ApplyExtendedProperties((2, objectId, paramOrdinal), parameterBuilder);
218
                parameterBuilder.WithAnnotation(SqlServerAnnotations.SqlDbType, sqlDbType.ToString());
219
            }
220

221
            if (b is StoredProcedureBuilder spb)
72!
222
                spb.AddParameter(Configure);
72✔
223
            else if (b is TableValuedFunctionBuilder tvfb)
×
224
                tvfb.AddParameter(Configure);
×
225
        }
226
    }
6✔
227

228

229
    private void ApplyExtendedProperties<TBuilder>(
230
        (int Class, int MajorId, int MinorId) key,
231
        TBuilder targetBuilder)
232
        where TBuilder : IAnnotationBuilder<TBuilder>
233
    {
234
        if (!_extendedProperties.TryGetValue(key, out var values))
2,070✔
235
            return;
2,039✔
236

237
        foreach (var (name, value) in values)
148✔
238
            targetBuilder.WithAnnotation(name, value);
43✔
239
    }
31✔
240

241

242
    private static string? GetEngineEditionName(int engineEdition)
243
    {
244
        return engineEdition switch
7!
245
        {
7✔
246
            1 => "Personal",
×
247
            2 => "Standard",
×
248
            3 => "Enterprise",
7✔
249
            4 => "Express",
×
250
            5 => "AzureSQLDatabase",
×
251
            6 => "AzureSynapseAnalytics",
×
252
            8 => "AzureSQLManagedInstance",
×
253
            9 => "AzureSQLEdge",
×
254
            11 => "AzureSynapseServerless",
×
255
            _ => "Unknown"
×
256
        };
7✔
257
    }
258

259

260
    private static string BuildTableFilter(SchemaReaderOptions options)
261
    {
262
        // Always filter out system objects (is_ms_shipped = 0) and then apply additional filters based on the specified schemas and tables if provided.
263
        var conditions = new List<string> { "t.is_ms_shipped = 0" };
7✔
264

265
        // If specific schemas are specified in the options, add a filter condition to include only those schemas.
266
        if (options.Schemas.Count > 0)
7!
267
        {
268
            var list = string.Join(", ", options.Schemas.Select(EscapeUnicodeLiteral));
×
269
            conditions.Add($"SCHEMA_NAME(t.schema_id) IN ({list})");
×
270
        }
271

272
        // If specific tables are specified in the options, add a filter condition to include only those tables.
273
        if (options.Tables.Count > 0)
7✔
274
        {
275
            var list = string.Join(", ", options.Tables.Select(EscapeUnicodeLiteral));
1✔
276
            conditions.Add($"t.name IN ({list})");
1✔
277
        }
278

279
        // Combine all conditions into a single WHERE clause string, joining them with "AND".
280
        return string.Join("\n    AND ", conditions);
7✔
281
    }
282

283
    private static string? BuildSchemaFilter(IReadOnlyCollection<string> schemas, string schemaExpression)
284
    {
285
        if (schemas.Count == 0)
37!
286
            return null;
37✔
287

288
        // Build a filter condition to include only the specified schemas.
289
        // The schemaExpression parameter allows specifying the expression to use
290
        // for the schema name (e.g. "SCHEMA_NAME(o.schema_id)" or "SCHEMA_NAME(t.schema_id)").
291

292
        var list = string.Join(", ", schemas.Select(EscapeUnicodeLiteral));
×
293

294
        // Return a filter condition like "SCHEMA_NAME(o.schema_id) IN ('schema1', 'schema2')".
295
        return $"{schemaExpression} IN ({list})";
×
296
    }
297

298

299
    private static string EscapeUnicodeLiteral(string value)
300
        => $"N{value.EscapeLiteral()}";
1✔
301

302

303
    private static string FormatNativeTypeName(string systemTypeName, string userTypeName, short maxLength, byte precision, byte scale)
304
    {
305
        // User-defined alias type — return the alias name as-is.
306
        if (!string.Equals(systemTypeName, userTypeName, StringComparison.OrdinalIgnoreCase))
1,756!
307
            return userTypeName;
×
308

309
        // For system types, format the native type name with length/precision/scale as appropriate for the type.
310
        // For example, for character types, include the max length (e.g. varchar(50)); for decimal/numeric, include precision and scale (e.g. decimal(18, 2));
311
        // for datetime2/time/datetimeoffset, include fractional seconds precision if it's not the default of 7 (e.g. datetime2(3)).
312

313
        var nativeTypeName = systemTypeName.ToUpperInvariant();
1,756✔
314

315
        if (IsTypeName(systemTypeName, "char") || IsTypeName(systemTypeName, "varchar") || IsTypeName(systemTypeName, "binary") || IsTypeName(systemTypeName, "varbinary"))
1,756!
316
            return maxLength == -1 ? $"{nativeTypeName}(MAX)" : $"{nativeTypeName}({maxLength})";
180✔
317

318
        if (IsTypeName(systemTypeName, "nchar") || IsTypeName(systemTypeName, "nvarchar"))
1,576✔
319
            return maxLength == -1 ? $"{nativeTypeName}(MAX)" : $"{nativeTypeName}({maxLength / 2})";
592✔
320

321
        if (IsTypeName(systemTypeName, "decimal") || IsTypeName(systemTypeName, "numeric"))
984✔
322
            return $"{nativeTypeName}({precision}, {scale})";
24✔
323

324
        if (IsTypeName(systemTypeName, "datetime2") || IsTypeName(systemTypeName, "datetimeoffset") || IsTypeName(systemTypeName, "time"))
960✔
325
            return scale != 7 ? $"{nativeTypeName}({scale})" : nativeTypeName;
188!
326

327
        return nativeTypeName;
772✔
328
    }
329

330
    private static int? NormalizeMaxLength(string systemTypeName, short maxLength)
331
    {
332
        if (IsTypeName(systemTypeName, "char") || IsTypeName(systemTypeName, "varchar") || IsTypeName(systemTypeName, "binary") || IsTypeName(systemTypeName, "varbinary"))
1,756!
333
            return maxLength == -1 ? null : maxLength;
180✔
334

335
        if (IsTypeName(systemTypeName, "nchar") || IsTypeName(systemTypeName, "nvarchar"))
1,576✔
336
            return maxLength == -1 ? null : maxLength / 2;
592✔
337

338
        return null;
984✔
339
    }
340

341
    private static bool HasPrecision(string systemTypeName)
342
        => IsTypeName(systemTypeName, "decimal")
1,762✔
343
        || IsTypeName(systemTypeName, "numeric");
1,762✔
344

345
    private static bool HasScale(string systemTypeName)
346
        => IsTypeName(systemTypeName, "decimal")
1,762✔
347
        || IsTypeName(systemTypeName, "numeric")
1,762✔
348
        || IsTypeName(systemTypeName, "datetime2")
1,762✔
349
        || IsTypeName(systemTypeName, "datetimeoffset")
1,762✔
350
        || IsTypeName(systemTypeName, "time");
1,762✔
351

352
    private static bool IsTypeName(string typeName, string expected)
353
        => string.Equals(typeName, expected, StringComparison.OrdinalIgnoreCase);
36,414✔
354

355
    private static ReferentialAction MapReferentialAction(byte action) => action switch
240!
356
    {
240✔
357
        1 => ReferentialAction.Cascade,
×
358
        2 => ReferentialAction.SetNull,
×
359
        3 => ReferentialAction.SetDefault,
×
360
        _ => ReferentialAction.NoAction,
240✔
361
    };
240✔
362
}
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