diff --git a/README.md b/README.md index 745d6cb..f159acd 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Console.WriteLine($"Tables: {model.Tables.Count}, Views: {model.Views.Count}"); var options = new SchemaReaderOptions { Schemas = ["dbo", "Sales"], + Tables = ["Customer", "Sales.Orders"], IncludeStoredProcedures = false, IncludeScalarFunctions = false, }; @@ -77,6 +78,14 @@ var options = new SchemaReaderOptions var model = await reader.ReadAsync(connectionString, options); ``` +`Tables` entries may be schema qualified (`"Sales.Orders"`), which pins the table to that +schema; the first dot separates the schema from the table name. Unqualified entries +(`"Customer"`) match that name in any schema. + +Names are passed to the database as written and compared with its own identifier rules. +A PostgreSQL table created unquoted as `Sales.Orders` is stored as `sales.orders` and has +to be filtered with that spelling; quoted identifiers keep their case. + ### JSON Serialization The model uses source-generated `System.Text.Json` serialization via `MetadataJsonContext` for AOT-friendly, allocation-light round-tripping. @@ -114,7 +123,7 @@ Supported providers are `SqlServer`, `PostgreSQL`, `MySQL`, `Oracle`, and `SQLit Filter the exported metadata with `--schema` and `--table`, and exclude object types with boolean switches: ```powershell -SchemaSaurus export -c "Server=.;Database=AdventureWorks;Integrated Security=true;TrustServerCertificate=true" -p SqlServer -o .\schema.json --schema dbo --table Customer --exclude-stored-procedures --exclude-sequences +SchemaSaurus export -c "Server=.;Database=AdventureWorks;Integrated Security=true;TrustServerCertificate=true" -p SqlServer -o .\schema.json --schema dbo --table Customer --table Sales.Orders --exclude-stored-procedures --exclude-sequences ``` Available exclusion switches are `--exclude-views`, `--exclude-stored-procedures`, `--exclude-scalar-functions`, `--exclude-table-valued-functions`, `--exclude-sequences`, and `--exclude-user-defined-types`. diff --git a/src/SchemaSaurus.Metadata/Internal/TableFilter.cs b/src/SchemaSaurus.Metadata/Internal/TableFilter.cs new file mode 100644 index 0000000..04eed33 --- /dev/null +++ b/src/SchemaSaurus.Metadata/Internal/TableFilter.cs @@ -0,0 +1,101 @@ +namespace SchemaSaurus.Metadata.Internal; + +/// +/// Builds provider filters from entries, +/// which may be bare names ("orders") or schema qualified ("sales.orders"). +/// +/// +/// Entries are split with . Schema names are carried +/// through to the generated SQL verbatim, so the database decides how they compare; +/// , which filters in memory, compares ordinal case-insensitively. +/// +public static class TableFilter +{ + /// + /// Builds a SQL condition that matches the specified table entries, pinning schema + /// qualified entries to their schema. + /// + /// The table entries to match; must not be empty. + /// The SQL expression yielding the schema name. + /// The SQL expression yielding the object name. + /// + /// Builds a provider specific set membership condition for the specified values and expression. + /// + /// A SQL condition such as (t.name IN ('orders') OR (s.name = 'sales' AND t.name IN ('orders'))). + public static string Build( + IReadOnlyCollection tables, + string schemaExpression, + string tableExpression, + Func, string, string> inClauseBuilder) + { + ArgumentNullException.ThrowIfNull(tables); + ArgumentNullException.ThrowIfNull(inClauseBuilder); + + var unqualified = new List(); + + // Schemas are kept in first-seen order so the generated condition is stable across calls. + var schemas = new List(); + var qualified = new Dictionary>(StringComparer.Ordinal); + + foreach (var table in tables) + { + var entry = SchemaQualifiedName.Parse(table); + var schema = entry.Schema; + + if (string.IsNullOrEmpty(schema)) + { + unqualified.Add(entry.Name); + continue; + } + + if (!qualified.TryGetValue(schema, out var names)) + { + names = []; + qualified[schema] = names; + schemas.Add(schema); + } + + names.Add(entry.Name); + } + + var conditions = new List(schemas.Count + 1); + + if (unqualified.Count > 0) + conditions.Add(inClauseBuilder(unqualified, tableExpression)); + + foreach (var schema in schemas) + conditions.Add($"({inClauseBuilder([schema], schemaExpression)} AND {inClauseBuilder(qualified[schema], tableExpression)})"); + + return conditions.Count == 1 + ? conditions[0] + : $"({string.Join(" OR ", conditions)})"; + } + + /// + /// Determines whether an object is matched by the specified table entries. + /// + /// The table entries to match; an empty collection matches everything. + /// The schema of the object, or when the provider has none. + /// The unqualified object name. + /// when the object is included; otherwise . + public static bool IsMatch(IReadOnlyCollection tables, string? schema, string name) + { + ArgumentNullException.ThrowIfNull(tables); + + if (tables.Count == 0) + return true; + + foreach (var table in tables) + { + var entry = SchemaQualifiedName.Parse(table); + + if (!string.Equals(entry.Name, name, StringComparison.OrdinalIgnoreCase)) + continue; + + if (string.IsNullOrEmpty(entry.Schema) || string.Equals(entry.Schema, schema, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } +} diff --git a/src/SchemaSaurus.Metadata/Provider/SchemaReaderOptions.cs b/src/SchemaSaurus.Metadata/Provider/SchemaReaderOptions.cs index fc3b64d..297fc3f 100644 --- a/src/SchemaSaurus.Metadata/Provider/SchemaReaderOptions.cs +++ b/src/SchemaSaurus.Metadata/Provider/SchemaReaderOptions.cs @@ -5,7 +5,8 @@ namespace SchemaSaurus.Metadata.Provider; /// reads a database schema. /// /// -/// All filter lists use ordinal case-insensitive matching. An empty list means "include all." +/// Filter list entries are passed to the provider as written and compared with the +/// database's own identifier rules. An empty list means "include all." /// Boolean flags default to so that a default-constructed instance /// captures a complete snapshot. /// @@ -19,8 +20,10 @@ public sealed class SchemaReaderOptions /// /// Table names to include. When empty, all tables are included. - /// Names are matched without schema qualification; combine with - /// for scoped filtering. + /// Entries may be schema qualified ("sales.orders"), which pins the table to that + /// schema; the first dot separates the schema from the table name. Unqualified entries + /// ("orders") match the name in any schema; combine with for + /// scoped filtering. /// public IReadOnlyList Tables { get; init; } = []; diff --git a/src/SchemaSaurus.MySql/MySqlSchemaReader.cs b/src/SchemaSaurus.MySql/MySqlSchemaReader.cs index 2bf2424..7b41284 100644 --- a/src/SchemaSaurus.MySql/MySqlSchemaReader.cs +++ b/src/SchemaSaurus.MySql/MySqlSchemaReader.cs @@ -6,6 +6,7 @@ using SchemaSaurus.Metadata; using SchemaSaurus.Metadata.Builders; using SchemaSaurus.Metadata.Extensions; +using SchemaSaurus.Metadata.Internal; using SchemaSaurus.Metadata.Provider; namespace SchemaSaurus.MySql; @@ -205,14 +206,17 @@ private static string BuildInformationSchemaTableFilter(SchemaReaderOptions opti conditions.Add(schemaFilter); if (options.Tables.Count > 0) - { - var list = string.Join(", ", options.Tables.Select(value => value.EscapeLiteral())); - conditions.Add($"{tableExpression} IN ({list})"); - } + conditions.Add(TableFilter.Build(options.Tables, schemaExpression, tableExpression, BuildInClause)); return string.Join("\n AND ", conditions); } + private static string BuildInClause(IReadOnlyCollection values, string expression) + { + var list = string.Join(", ", values.Select(value => value.EscapeLiteral())); + return $"{expression} IN ({list})"; + } + private static string? BuildSchemaFilter(IReadOnlyCollection schemas, string schemaExpression) { if (schemas.Count == 0) diff --git a/src/SchemaSaurus.Oracle/OracleSchemaReader.cs b/src/SchemaSaurus.Oracle/OracleSchemaReader.cs index 20b574b..86f5f88 100644 --- a/src/SchemaSaurus.Oracle/OracleSchemaReader.cs +++ b/src/SchemaSaurus.Oracle/OracleSchemaReader.cs @@ -190,10 +190,7 @@ private static string BuildObjectFilter(SchemaReaderOptions options, string sche List conditions = [BuildSchemaFilter(options.Schemas, schemaExpression)]; if (options.Tables.Count > 0) - { - var tableFilter = BuildCaseInsensitiveFilter(options.Tables, objectExpression); - conditions.Add(tableFilter); - } + conditions.Add(TableFilter.Build(options.Tables, schemaExpression, objectExpression, BuildCaseInsensitiveFilter)); return string.Join("\n AND ", conditions); } diff --git a/src/SchemaSaurus.PostgreSql/PostgreSqlSchemaReader.cs b/src/SchemaSaurus.PostgreSql/PostgreSqlSchemaReader.cs index 44b081b..3aa742e 100644 --- a/src/SchemaSaurus.PostgreSql/PostgreSqlSchemaReader.cs +++ b/src/SchemaSaurus.PostgreSql/PostgreSqlSchemaReader.cs @@ -5,6 +5,7 @@ using SchemaSaurus.Metadata; using SchemaSaurus.Metadata.Builders; using SchemaSaurus.Metadata.Extensions; +using SchemaSaurus.Metadata.Internal; using SchemaSaurus.Metadata.Provider; namespace SchemaSaurus.PostgreSql; @@ -244,14 +245,17 @@ private static string BuildTableFilter(SchemaReaderOptions options, string schem conditions.Add(schemaFilter); if (options.Tables.Count > 0) - { - var list = string.Join(", ", options.Tables.Select(value => value.EscapeLiteral())); - conditions.Add($"{tableExpression} IN ({list})"); - } + conditions.Add(TableFilter.Build(options.Tables, schemaExpression, tableExpression, BuildInClause)); return string.Join("\n AND ", conditions); } + private static string BuildInClause(IReadOnlyCollection values, string expression) + { + var list = string.Join(", ", values.Select(value => value.EscapeLiteral())); + return $"{expression} IN ({list})"; + } + private static string? BuildSchemaFilter(IReadOnlyCollection schemas, string schemaExpression) { if (schemas.Count == 0) diff --git a/src/SchemaSaurus.SqlServer/SqlServerSchemaReader.cs b/src/SchemaSaurus.SqlServer/SqlServerSchemaReader.cs index 932b4c8..e7c814b 100644 --- a/src/SchemaSaurus.SqlServer/SqlServerSchemaReader.cs +++ b/src/SchemaSaurus.SqlServer/SqlServerSchemaReader.cs @@ -6,6 +6,7 @@ using SchemaSaurus.Metadata; using SchemaSaurus.Metadata.Builders; using SchemaSaurus.Metadata.Extensions; +using SchemaSaurus.Metadata.Internal; using SchemaSaurus.Metadata.Provider; namespace SchemaSaurus.SqlServer; @@ -276,10 +277,7 @@ private static string BuildTableFilter(SchemaReaderOptions options) // If specific tables are specified in the options, add a filter condition to include only those tables. if (options.Tables.Count > 0) - { - var list = string.Join(", ", options.Tables.Select(EscapeUnicodeLiteral)); - conditions.Add($"t.name IN ({list})"); - } + conditions.Add(TableFilter.Build(options.Tables, "SCHEMA_NAME(t.schema_id)", "t.name", BuildInClause)); // Combine all conditions into a single WHERE clause string, joining them with "AND". return string.Join("\n AND ", conditions); @@ -301,6 +299,13 @@ private static string BuildTableFilter(SchemaReaderOptions options) } + private static string BuildInClause(IReadOnlyCollection values, string expression) + { + var list = string.Join(", ", values.Select(EscapeUnicodeLiteral)); + return $"{expression} IN ({list})"; + } + + private static string EscapeUnicodeLiteral(string value) => $"N{value.EscapeLiteral()}"; diff --git a/src/SchemaSaurus.Sqlite/SqliteSchemaReader.Tables.cs b/src/SchemaSaurus.Sqlite/SqliteSchemaReader.Tables.cs index 27b96f2..0f8ccf3 100644 --- a/src/SchemaSaurus.Sqlite/SqliteSchemaReader.Tables.cs +++ b/src/SchemaSaurus.Sqlite/SqliteSchemaReader.Tables.cs @@ -1,6 +1,7 @@ using Microsoft.Data.Sqlite; using SchemaSaurus.Metadata.Builders; +using SchemaSaurus.Metadata.Internal; using SchemaSaurus.Metadata.Provider; namespace SchemaSaurus.Sqlite; @@ -68,9 +69,7 @@ ORDER BY name continue; } - // Keep the provider-side filter case-insensitive to match SQLite object lookup behavior. - if (options.Tables.Count > 0 - && !options.Tables.Contains(name, StringComparer.OrdinalIgnoreCase)) + if (!TableFilter.IsMatch(options.Tables, MainSchemaName, name)) { continue; } diff --git a/src/SchemaSaurus.Sqlite/SqliteSchemaReader.Views.cs b/src/SchemaSaurus.Sqlite/SqliteSchemaReader.Views.cs index fb925ec..10dc7a2 100644 --- a/src/SchemaSaurus.Sqlite/SqliteSchemaReader.Views.cs +++ b/src/SchemaSaurus.Sqlite/SqliteSchemaReader.Views.cs @@ -1,6 +1,7 @@ using Microsoft.Data.Sqlite; using SchemaSaurus.Metadata.Builders; +using SchemaSaurus.Metadata.Internal; using SchemaSaurus.Metadata.Extensions; using SchemaSaurus.Metadata.Provider; @@ -34,7 +35,7 @@ ORDER BY name { var viewName = reader.GetString(viewNameOrdinal); if (IsSpatialiteObject(viewName) - || (options.Tables.Count > 0 && !options.Tables.Contains(viewName, StringComparer.OrdinalIgnoreCase))) + || !TableFilter.IsMatch(options.Tables, MainSchemaName, viewName)) { continue; } diff --git a/src/SchemaSaurus.Sqlite/SqliteSchemaReader.cs b/src/SchemaSaurus.Sqlite/SqliteSchemaReader.cs index 4c10a66..09e7b93 100644 --- a/src/SchemaSaurus.Sqlite/SqliteSchemaReader.cs +++ b/src/SchemaSaurus.Sqlite/SqliteSchemaReader.cs @@ -110,6 +110,9 @@ private static async Task ReadDatabasePragmasAsync( // Note: SQLite does not support sequences, stored procedures, functions, or user-defined types. + // Schema qualified filter entries are matched against SQLite's default attached database. + private const string MainSchemaName = "main"; + private static bool IsSpatialiteObject(string name) => SpatialiteObjectNames.Contains(name, StringComparer.Ordinal); } diff --git a/test/SchemaSaurus.Metadata.Tests/Internal/TableFilterTests.cs b/test/SchemaSaurus.Metadata.Tests/Internal/TableFilterTests.cs new file mode 100644 index 0000000..cefc53c --- /dev/null +++ b/test/SchemaSaurus.Metadata.Tests/Internal/TableFilterTests.cs @@ -0,0 +1,53 @@ +using SchemaSaurus.Metadata.Extensions; +using SchemaSaurus.Metadata.Internal; + +namespace SchemaSaurus.Metadata.Tests.Internal; + +public class TableFilterTests +{ + [Fact] + public void WhenTablesQualifiedThenSchemaPinned() + { + var filter = TableFilter.Build(["sales.orders", "sales.customers"], "ns.nspname", "cls.relname", BuildInClause); + + filter.Should().Be("(ns.nspname IN ('sales') AND cls.relname IN ('orders', 'customers'))"); + } + + [Fact] + public void WhenTablesMixedThenConditionsCombinedWithOr() + { + var filter = TableFilter.Build(["orders", "shipping.orders"], "ns.nspname", "cls.relname", BuildInClause); + + filter.Should().Be("(cls.relname IN ('orders') OR (ns.nspname IN ('shipping') AND cls.relname IN ('orders')))"); + } + + [Fact] + public void WhenSchemasDifferOnlyByCaseThenEachKeepsItsOwnSpelling() + { + var filter = TableFilter.Build(["Sales.orders", "sales.customers"], "ns.nspname", "cls.relname", BuildInClause); + + filter.Should().Be( + "((ns.nspname IN ('Sales') AND cls.relname IN ('orders')) " + + "OR (ns.nspname IN ('sales') AND cls.relname IN ('customers')))"); + } + + [Fact] + public void WhenEntryHasMultipleDotsThenFirstDotSeparatesSchema() + { + var filter = TableFilter.Build(["catalog.sales.orders"], "ns.nspname", "cls.relname", BuildInClause); + + filter.Should().Be("(ns.nspname IN ('catalog') AND cls.relname IN ('sales.orders'))"); + } + + [Fact] + public void WhenEntryQualifiedThenOnlyMatchingSchemaMatched() + { + TableFilter.IsMatch([], "main", "orders").Should().BeTrue(); + TableFilter.IsMatch(["orders"], "main", "ORDERS").Should().BeTrue(); + TableFilter.IsMatch(["main.orders"], "main", "orders").Should().BeTrue(); + TableFilter.IsMatch(["sales.orders"], "main", "orders").Should().BeFalse(); + } + + private static string BuildInClause(IReadOnlyCollection values, string expression) + => $"{expression} IN ({string.Join(", ", values.Select(value => value.EscapeLiteral()))})"; +} diff --git a/test/SchemaSaurus.MySql.Tests/TableSchemaTests.cs b/test/SchemaSaurus.MySql.Tests/TableSchemaTests.cs index c24c124..012d3a5 100644 --- a/test/SchemaSaurus.MySql.Tests/TableSchemaTests.cs +++ b/test/SchemaSaurus.MySql.Tests/TableSchemaTests.cs @@ -261,6 +261,35 @@ public async Task WhenFilteringByTableNameThenOnlyMatchingTablesReturned() model.Tables[0].QualifiedName.Name.Should().Be("Status"); } + [Fact] + public async Task WhenFilteringBySchemaQualifiedTableNameThenOnlyThatSchemaMatched() + { + var expected = (await GetDatabaseModelAsync()).Tables.First(t => t.QualifiedName.Name == "Status"); + + var options = new Metadata.Provider.SchemaReaderOptions + { + Tables = [$"{expected.QualifiedName.Schema}.Status"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().ContainSingle() + .Which.QualifiedName.Should().Be(expected.QualifiedName); + } + + [Fact] + public async Task WhenFilteringByUnknownSchemaThenNoTablesReturned() + { + var options = new Metadata.Provider.SchemaReaderOptions + { + Tables = ["Missing.Status"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().BeEmpty(); + } + [Fact] public async Task WhenReadingColumnsOrdinalPositionsArePopulated() { diff --git a/test/SchemaSaurus.Oracle.Tests/TableSchemaTests.cs b/test/SchemaSaurus.Oracle.Tests/TableSchemaTests.cs index ad483aa..9ececf0 100644 --- a/test/SchemaSaurus.Oracle.Tests/TableSchemaTests.cs +++ b/test/SchemaSaurus.Oracle.Tests/TableSchemaTests.cs @@ -227,6 +227,35 @@ public async Task WhenFilteringByTableNameThenOnlyMatchingTablesReturned() model.Tables[0].QualifiedName.Name.Should().Be(StatusTableName); } + [Fact] + public async Task WhenFilteringBySchemaQualifiedTableNameThenOnlyThatSchemaMatched() + { + var expected = (await GetDatabaseModelAsync()).Tables.First(t => t.QualifiedName.Name == StatusTableName); + + var options = new Metadata.Provider.SchemaReaderOptions + { + Tables = [$"{expected.QualifiedName.Schema}.Status"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().ContainSingle() + .Which.QualifiedName.Should().Be(expected.QualifiedName); + } + + [Fact] + public async Task WhenFilteringByUnknownSchemaThenNoTablesReturned() + { + var options = new Metadata.Provider.SchemaReaderOptions + { + Tables = ["Missing.Status"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().BeEmpty(); + } + [Fact] public async Task WhenReadingColumnsOrdinalPositionsArePopulated() { diff --git a/test/SchemaSaurus.PostgreSql.Tests/TableSchemaTests.cs b/test/SchemaSaurus.PostgreSql.Tests/TableSchemaTests.cs index ef44edc..9971b20 100644 --- a/test/SchemaSaurus.PostgreSql.Tests/TableSchemaTests.cs +++ b/test/SchemaSaurus.PostgreSql.Tests/TableSchemaTests.cs @@ -249,6 +249,51 @@ public async Task WhenFilteringByTableNameThenOnlyMatchingTablesReturned() model.Tables[0].QualifiedName.Name.Should().Be("Status"); } + [Fact] + public async Task WhenFilteringByUnqualifiedTableNameThenAllSchemasMatched() + { + var options = new Metadata.Provider.SchemaReaderOptions + { + Tables = ["Duplicate"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().HaveCount(2); + model.Tables.Should().Contain(t => t.QualifiedName.Schema == "public"); + model.Tables.Select(t => t.QualifiedName.Schema).Should().OnlyHaveUniqueItems(); + } + + [Fact] + public async Task WhenFilteringBySchemaQualifiedTableNameThenOnlyThatSchemaMatched() + { + var expected = (await GetDatabaseModelAsync()).Tables + .First(t => t.QualifiedName.Name == "Duplicate" && t.QualifiedName.Schema != "public"); + + var options = new Metadata.Provider.SchemaReaderOptions + { + Tables = [$"{expected.QualifiedName.Schema}.Duplicate"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().ContainSingle() + .Which.QualifiedName.Should().Be(expected.QualifiedName); + } + + [Fact] + public async Task WhenFilteringByUnknownSchemaThenNoTablesReturned() + { + var options = new Metadata.Provider.SchemaReaderOptions + { + Tables = ["Missing.Duplicate"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().BeEmpty(); + } + [Fact] public async Task WhenReadingColumnsOrdinalPositionsArePopulated() { diff --git a/test/SchemaSaurus.SqlServer.Tests/TableSchemaTests.cs b/test/SchemaSaurus.SqlServer.Tests/TableSchemaTests.cs index 288e6f5..cda6925 100644 --- a/test/SchemaSaurus.SqlServer.Tests/TableSchemaTests.cs +++ b/test/SchemaSaurus.SqlServer.Tests/TableSchemaTests.cs @@ -245,6 +245,51 @@ public async Task WhenFilteringByTableNameThenOnlyMatchingTablesReturned() model.Tables[0].QualifiedName.Name.Should().Be("Status"); } + [Fact] + public async Task WhenFilteringByUnqualifiedTableNameThenAllSchemasMatched() + { + var options = new Metadata.Provider.SchemaReaderOptions + { + Tables = ["Duplicate"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().HaveCount(2); + model.Tables.Should().Contain(t => t.QualifiedName.Schema == "dbo"); + model.Tables.Select(t => t.QualifiedName.Schema).Should().OnlyHaveUniqueItems(); + } + + [Fact] + public async Task WhenFilteringBySchemaQualifiedTableNameThenOnlyThatSchemaMatched() + { + var expected = (await GetDatabaseModelAsync()).Tables + .First(t => t.QualifiedName.Name == "Duplicate" && t.QualifiedName.Schema != "dbo"); + + var options = new Metadata.Provider.SchemaReaderOptions + { + Tables = [$"{expected.QualifiedName.Schema}.Duplicate"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().ContainSingle() + .Which.QualifiedName.Should().Be(expected.QualifiedName); + } + + [Fact] + public async Task WhenFilteringByUnknownSchemaThenNoTablesReturned() + { + var options = new Metadata.Provider.SchemaReaderOptions + { + Tables = ["Missing.Duplicate"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().BeEmpty(); + } + [Fact] public async Task WhenReadingColumnsOrdinalPositionsArePopulated() { diff --git a/test/SchemaSaurus.Sqlite.Tests/TableSchemaTests.cs b/test/SchemaSaurus.Sqlite.Tests/TableSchemaTests.cs index dce4315..a6594cf 100644 --- a/test/SchemaSaurus.Sqlite.Tests/TableSchemaTests.cs +++ b/test/SchemaSaurus.Sqlite.Tests/TableSchemaTests.cs @@ -223,6 +223,33 @@ public async Task WhenFilteringByTableNameThenOnlyMatchingTablesReturned() model.Tables[0].QualifiedName.Name.Should().Be("Status"); } + [Fact] + public async Task WhenFilteringByMainSchemaQualifiedTableNameThenTableReturned() + { + var options = new SchemaReaderOptions + { + Tables = ["main.Status"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().ContainSingle() + .Which.QualifiedName.Name.Should().Be("Status"); + } + + [Fact] + public async Task WhenFilteringByOtherSchemaQualifiedTableNameThenNoTablesReturned() + { + var options = new SchemaReaderOptions + { + Tables = ["other.Status"] + }; + + var model = await GetDatabaseModelAsync(options); + + model.Tables.Should().BeEmpty(); + } + [Fact] public async Task WhenReadingColumnsOrdinalPositionsArePopulated() {