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()
{