feat(sql): construct Postgres and Oracle providers; MySql/Odbc stay reserved
Widens the two provider gates so an authored `{"provider":"Postgres"}` or
`{"provider":"Oracle"}` binds that provider's own dialect instead of failing
"not available in this build":
- `SqlDriverFactoryExtensions.CreateDialect` gains a Postgres and an Oracle
arm. There is deliberately no fallback arm — a default resolving to
SqlServerDialect would construct happily and then emit T-SQL at a Postgres
or Oracle server. The rejection message now names the three supported and
the two still-reserved providers.
- `SqlDriverBrowser.DefaultDialectSelector` widens identically, so a provider
the runtime constructs is also browsable; MySql/Odbc keep resolving to null
and the caller keeps reporting them as unavailable.
Tests: the existing rejected-provider controls flip from Postgres to MySql
(factory) and Odbc (browser) so the reserved half stays falsifiable, and the
new legs assert the bound dialect TYPE rather than mere construction —
"no throw" alone would pass against a SqlServer fallback.
Doc-comment sweep (contract addition, one line each): the "v1 constructs
SqlServer only" / "reserved for P2" claims in SqlProvider, SqlDriverConfigDto,
ISqlDialect.Provider and SqlServerDialect are now false and were corrected in
the same change.
This commit is contained in:
@@ -45,12 +45,21 @@ public sealed class SqlDriverBrowserTests
|
||||
new SqlDriverBrowser().DriverType.ShouldBe(SqlDriver.DriverTypeName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The browse-side gate, asserted as a partition of the whole enum: the three carried providers each
|
||||
/// resolve to <b>their own</b> dialect, and every remaining member resolves to <see langword="null"/>.
|
||||
/// Enumerating <see cref="Enum.GetValues{TEnum}"/> for the reserved half is what makes a future enum
|
||||
/// member fail here rather than silently browse as SQL Server.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DefaultDialectSelector_carriesSqlServerOnly()
|
||||
public void DefaultDialectSelector_carriesSqlServerPostgresAndOracle()
|
||||
{
|
||||
SqlDriverBrowser.DefaultDialectSelector(SqlProvider.SqlServer).ShouldBeOfType<SqlServerDialect>();
|
||||
SqlDriverBrowser.DefaultDialectSelector(SqlProvider.Postgres).ShouldBeOfType<PostgresDialect>();
|
||||
SqlDriverBrowser.DefaultDialectSelector(SqlProvider.Oracle).ShouldBeOfType<OracleDialect>();
|
||||
|
||||
foreach (var provider in Enum.GetValues<SqlProvider>().Where(p => p != SqlProvider.SqlServer))
|
||||
SqlProvider[] carried = [SqlProvider.SqlServer, SqlProvider.Postgres, SqlProvider.Oracle];
|
||||
foreach (var provider in Enum.GetValues<SqlProvider>().Where(p => !carried.Contains(p)))
|
||||
SqlDriverBrowser.DefaultDialectSelector(provider).ShouldBeNull();
|
||||
}
|
||||
|
||||
@@ -178,6 +187,10 @@ public sealed class SqlDriverBrowserTests
|
||||
|
||||
// ---- provider availability ----
|
||||
|
||||
/// <summary>
|
||||
/// The falsifiability control for the widened browse gate: <see cref="SqlProvider.MySql"/> is still
|
||||
/// reserved and must be reported as unavailable, naming itself.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Open_providerThisBuildDoesNotCarry_saysNotAvailableInThisBuild()
|
||||
{
|
||||
@@ -185,15 +198,15 @@ public sealed class SqlDriverBrowserTests
|
||||
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(() =>
|
||||
browser.OpenAsync(
|
||||
ConfigJson("Postgres", connectionStringRef: "MesStaging"), CancellationToken.None));
|
||||
ConfigJson("MySql", connectionStringRef: "MesStaging"), CancellationToken.None));
|
||||
|
||||
ex.Message.ShouldContain("Postgres");
|
||||
ex.Message.ShouldContain("MySql");
|
||||
ex.Message.ShouldContain("not available in this build");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provider availability is answered before the environment is touched: "this build cannot browse
|
||||
/// Postgres" is true whatever the ref resolves to, and the two failures must not mask each other.
|
||||
/// MySql" is true whatever the ref resolves to, and the two failures must not mask each other.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Open_unavailableProviderAndUnresolvableRef_reportsTheProvider()
|
||||
@@ -203,12 +216,34 @@ public sealed class SqlDriverBrowserTests
|
||||
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(() =>
|
||||
browser.OpenAsync(
|
||||
ConfigJson("Oracle", connectionStringRef: "MesStaging"), CancellationToken.None));
|
||||
ConfigJson("Odbc", connectionStringRef: "MesStaging"), CancellationToken.None));
|
||||
|
||||
ex.Message.ShouldContain("not available in this build");
|
||||
reads.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A carried provider must get <b>past</b> the provider gate and on to the environment, so that a
|
||||
/// Postgres or Oracle browse fails (if at all) on the connection string rather than on availability.
|
||||
/// Asserted through the reads list: reaching the environment reader is the observable proof the gate
|
||||
/// let the provider through.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("Postgres")]
|
||||
[InlineData("Oracle")]
|
||||
public async Task Open_aCarriedProvider_passesTheGateAndResolvesTheRef(string provider)
|
||||
{
|
||||
var reads = new List<string>();
|
||||
var browser = new SqlDriverBrowser(environmentReader: name => { reads.Add(name); return null; });
|
||||
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(() =>
|
||||
browser.OpenAsync(
|
||||
ConfigJson(provider, connectionStringRef: "MesStaging"), CancellationToken.None));
|
||||
|
||||
ex.Message.ShouldNotContain("not available in this build");
|
||||
reads.ShouldContain(SqlDriverBrowser.EnvironmentVariableFor("MesStaging"));
|
||||
}
|
||||
|
||||
// ---- malformed configuration ----
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -122,6 +122,11 @@ public sealed class SqlDriverFactoryTests
|
||||
|
||||
// ---- provider gate ----
|
||||
|
||||
/// <summary>
|
||||
/// The falsifiability control for the widened gate: <see cref="SqlProvider.MySql"/> is still a
|
||||
/// reserved name and must fail loudly at construction, naming itself. Without this leg, a gate that
|
||||
/// had quietly become "construct anything" would pass every test below.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateInstance_aProviderThisBuildCannotConstruct_throwsSayingSo()
|
||||
{
|
||||
@@ -130,12 +135,73 @@ public sealed class SqlDriverFactoryTests
|
||||
SqlConnectionStringResolver.EnvironmentVariableFor(name), "Host=h;Database=db");
|
||||
|
||||
var ex = Should.Throw<InvalidOperationException>(() => SqlDriverFactoryExtensions.CreateInstance(
|
||||
"d1", $$"""{"provider":"Postgres","connectionStringRef":"{{name}}"}""", null));
|
||||
"d1", $$"""{"provider":"MySql","connectionStringRef":"{{name}}"}""", null));
|
||||
|
||||
ex.Message.ShouldContain("Postgres");
|
||||
ex.Message.ShouldContain("MySql");
|
||||
ex.Message.ShouldContain("not available in this build");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The reserved names are named as reserved — an operator reading the rejection must be able to tell
|
||||
/// "misspelled" from "this build does not carry it", so the message enumerates both halves.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateInstance_aReservedProvider_namesTheSupportedAndTheReservedSets()
|
||||
{
|
||||
var name = NewRef();
|
||||
using var _ = new EnvironmentVariableScope(
|
||||
SqlConnectionStringResolver.EnvironmentVariableFor(name), "Host=h;Database=db");
|
||||
|
||||
var ex = Should.Throw<InvalidOperationException>(() => SqlDriverFactoryExtensions.CreateInstance(
|
||||
"d1", $$"""{"provider":"Odbc","connectionStringRef":"{{name}}"}""", null));
|
||||
|
||||
ex.Message.ShouldContain(nameof(SqlProvider.SqlServer));
|
||||
ex.Message.ShouldContain(nameof(SqlProvider.Postgres));
|
||||
ex.Message.ShouldContain(nameof(SqlProvider.Oracle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The widened gate, asserted on the thing that actually varies: which dialect the config binds to.
|
||||
/// Binding — not merely "no throw" — is what proves the provider reaches the poll path, since a gate
|
||||
/// that fell through to <c>SqlServerDialect</c> would also construct without complaint and then speak
|
||||
/// T-SQL at a Postgres server.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("SqlServer", typeof(SqlServerDialect))]
|
||||
[InlineData("Postgres", typeof(PostgresDialect))]
|
||||
[InlineData("Oracle", typeof(OracleDialect))]
|
||||
public void ParseBinding_aSupportedProvider_bindsThatProvidersDialect(string provider, Type expected)
|
||||
{
|
||||
var name = NewRef();
|
||||
using var _ = new EnvironmentVariableScope(
|
||||
SqlConnectionStringResolver.EnvironmentVariableFor(name), "Host=h;Database=db");
|
||||
|
||||
var binding = SqlDriverFactoryExtensions.ParseBinding(
|
||||
"d1", $$"""{"provider":"{{provider}}","connectionStringRef":"{{name}}"}""");
|
||||
|
||||
binding.Dialect.ShouldBeOfType(expected);
|
||||
binding.Dialect.Provider.ShouldBe(Enum.Parse<SqlProvider>(provider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end through the registered factory entry point, so the widening is proven on the path the
|
||||
/// bootstrapper actually calls — not only on the internal parse.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("Postgres")]
|
||||
[InlineData("Oracle")]
|
||||
public async Task CreateInstance_aNewlySupportedProvider_constructsPastTheProviderGate(string provider)
|
||||
{
|
||||
var name = NewRef();
|
||||
using var _ = new EnvironmentVariableScope(
|
||||
SqlConnectionStringResolver.EnvironmentVariableFor(name), "Host=h;Database=db");
|
||||
|
||||
await using var driver = SqlDriverFactoryExtensions.CreateInstance(
|
||||
"d1", $$"""{"provider":"{{provider}}","connectionStringRef":"{{name}}"}""", null);
|
||||
|
||||
driver.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// ---- config validation (the reader and the shell deliberately do NOT do this) ----
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user