diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser/SqlDriverBrowser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser/SqlDriverBrowser.cs
index 2b0bd7a1..e67d5bd8 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser/SqlDriverBrowser.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser/SqlDriverBrowser.cs
@@ -80,8 +80,8 @@ public sealed class SqlDriverBrowser : IDriverBrowser
///
/// Maps an authored onto the dialect that browses it, or
/// for a provider this build does not carry. Defaults to
- /// (SqlServer only, in v1). A constructor parameter rather than a test hatch: it is also how a P2
- /// provider gets added without touching this class.
+ /// (SqlServer, Postgres and Oracle). A constructor parameter rather than a test hatch: it is also how
+ /// a further provider gets added without touching this class.
///
///
/// Reads one environment variable. Defaults to .
@@ -117,14 +117,23 @@ public sealed class SqlDriverBrowser : IDriverBrowser
ConnectionStringEnvironmentPrefix + connectionStringRef;
///
- /// The providers this build can browse. v1 constructs SQL Server only; every other
- /// member is a reserved name, so it resolves to and
- /// the caller reports it as unavailable rather than crashing on a null dialect.
+ /// The providers this build can browse — SQL Server, PostgreSQL and Oracle. The remaining
+ /// members (, )
+ /// are reserved names, so they resolve to and the caller reports them as
+ /// unavailable rather than crashing on a null dialect.
+ /// Kept in step with SqlDriverFactoryExtensions.CreateDialect: a provider the runtime
+ /// driver constructs but the browser cannot select would leave the operator authoring tags by hand
+ /// against a database the AdminUI refuses to show them.
///
/// The authored provider.
/// The dialect, or when this build does not carry the provider.
- public static ISqlDialect? DefaultDialectSelector(SqlProvider provider) =>
- provider == SqlProvider.SqlServer ? new SqlServerDialect() : null;
+ public static ISqlDialect? DefaultDialectSelector(SqlProvider provider) => provider switch
+ {
+ SqlProvider.SqlServer => new SqlServerDialect(),
+ SqlProvider.Postgres => new PostgresDialect(),
+ SqlProvider.Oracle => new OracleDialect(),
+ _ => null,
+ };
///
///
@@ -143,12 +152,13 @@ public sealed class SqlDriverBrowser : IDriverBrowser
var literal = ReadPastedConnectionString(configJson);
var config = Deserialize(configJson, literal);
- // Provider first: "this build cannot browse Postgres" is true regardless of how the database is
+ // Provider first: "this build cannot browse MySql" is true regardless of how the database is
// named, and answering it before touching the environment keeps the two failures from masking.
var dialect = _dialectSelector(config.Provider)
?? throw new InvalidOperationException(
- $"Sql provider '{config.Provider}' is not available in this build; v1 browses "
- + $"'{SqlProvider.SqlServer}' only.");
+ $"Sql provider '{config.Provider}' is not available in this build; browsing supports "
+ + $"'{SqlProvider.SqlServer}', '{SqlProvider.Postgres}' and '{SqlProvider.Oracle}' "
+ + $"('{SqlProvider.MySql}' and '{SqlProvider.Odbc}' are reserved names).");
var reference = config.ConnectionStringRef;
var connectionString = ResolveConnectionString(literal, reference);
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts/SqlDriverConfigDto.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts/SqlDriverConfigDto.cs
index 2bca5d24..1e29f05c 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts/SqlDriverConfigDto.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts/SqlDriverConfigDto.cs
@@ -16,7 +16,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
///
public sealed class SqlDriverConfigDto
{
- /// Which ADO.NET provider/dialect backs this instance. Defaults to — the only provider v1 constructs.
+ /// Which ADO.NET provider/dialect backs this instance. Defaults to ; and are also constructed.
public SqlProvider Provider { get; init; } = SqlProvider.SqlServer;
///
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts/SqlProvider.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts/SqlProvider.cs
index 67879307..0dd44c55 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts/SqlProvider.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts/SqlProvider.cs
@@ -1,21 +1,25 @@
namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
-/// Which ADO.NET provider/dialect backs a Sql driver instance. v1 constructs only SqlServer.
+///
+/// Which ADO.NET provider/dialect backs a Sql driver instance. Constructed: SqlServer, Postgres,
+/// Oracle — each over a fully managed provider package, no native client install. Reserved: MySql
+/// and Odbc are declared names only; authoring one fails at driver construction and at browse.
+///
public enum SqlProvider
{
- /// Microsoft SQL Server via Microsoft.Data.SqlClient — the only provider v1 constructs.
+ /// Microsoft SQL Server via Microsoft.Data.SqlClient.
SqlServer,
- /// PostgreSQL (Npgsql) — reserved for P2; not constructed in v1.
+ /// PostgreSQL via the managed Npgsql provider.
Postgres,
- /// MySQL / MariaDB — reserved; not constructed in v1.
+ /// MySQL / MariaDB — reserved; no dialect ships, so this is not constructed.
MySql,
- /// Generic ODBC — reserved for P2; not constructed in v1.
+ /// Generic ODBC — reserved; no dialect ships, so this is not constructed.
Odbc,
- /// Oracle — reserved; not constructed in v1.
+ /// Oracle Database via the fully managed Oracle.ManagedDataAccess.Core provider.
Oracle,
}
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/ISqlDialect.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/ISqlDialect.cs
index 35096a4c..80fea8d4 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/ISqlDialect.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/ISqlDialect.cs
@@ -28,7 +28,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Sql;
///
public interface ISqlDialect
{
- /// Which backend this dialect speaks. v1 constructs only.
+ ///
+ /// Which backend this dialect speaks. One implementation ships per constructed
+ /// member — ,
+ /// and .
+ ///
SqlProvider Provider { get; }
///
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/SqlServerDialect.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/SqlServerDialect.cs
index 2bbb1a0a..aafce4e7 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/SqlServerDialect.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/SqlServerDialect.cs
@@ -7,7 +7,7 @@ using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
namespace ZB.MOM.WW.OtOpcUa.Driver.Sql;
///
-/// Microsoft SQL Server dialect (design §2.2) — the only provider v1 constructs. Bracket quoting,
+/// Microsoft SQL Server dialect (design §2.2) — the default provider. Bracket quoting,
/// INFORMATION_SCHEMA catalog SQL, and the design §3.7 type-family map.
/// Stateless and immutable, so a single instance is safely shared by the driver's poll reader and
/// the AdminUI browse session.
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverFactoryExtensions.cs
index a4ec55ef..5961e670 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverFactoryExtensions.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverFactoryExtensions.cs
@@ -201,17 +201,27 @@ public static class SqlDriverFactoryExtensions
///
/// Selects the provider seam.
- /// v1 constructs only. The other members exist on the shipped
- /// enum as reserved names; authoring one is a config mistake that must fail at construction with a
- /// message that says so, rather than silently degrading to T-SQL against a non-SQL-Server database.
+ /// This build constructs ,
+ /// and . and
+ /// remain reserved names on the shipped enum; authoring one is a config
+ /// mistake that must fail at construction with a message that says so, rather than silently degrading
+ /// to another backend's SQL dialect against a database that does not speak it.
+ /// Every arm returns that provider's OWN dialect — there is no fallback arm. A default
+ /// that resolved to would construct happily and then emit T-SQL
+ /// (INFORMATION_SCHEMA, TOP 1, @ markers) at a Postgres or Oracle server, which
+ /// fails at poll time as an unintelligible provider error rather than here as a config error.
///
private static ISqlDialect CreateDialect(SqlProvider provider, string driverInstanceId)
=> provider switch
{
SqlProvider.SqlServer => new SqlServerDialect(),
+ SqlProvider.Postgres => new PostgresDialect(),
+ SqlProvider.Oracle => new OracleDialect(),
_ => throw new InvalidOperationException(
$"Sql driver config for '{driverInstanceId}' names provider '{provider}', which is not " +
- $"available in this build. Only '{nameof(SqlProvider.SqlServer)}' is supported."),
+ $"available in this build. Supported: '{nameof(SqlProvider.SqlServer)}', " +
+ $"'{nameof(SqlProvider.Postgres)}', '{nameof(SqlProvider.Oracle)}'. Reserved (declared on " +
+ $"the enum but not implemented): '{nameof(SqlProvider.MySql)}', '{nameof(SqlProvider.Odbc)}'."),
};
///
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.Tests/SqlDriverBrowserTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.Tests/SqlDriverBrowserTests.cs
index c984c7bd..93d1f29f 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.Tests/SqlDriverBrowserTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.Tests/SqlDriverBrowserTests.cs
@@ -45,12 +45,21 @@ public sealed class SqlDriverBrowserTests
new SqlDriverBrowser().DriverType.ShouldBe(SqlDriver.DriverTypeName);
}
+ ///
+ /// The browse-side gate, asserted as a partition of the whole enum: the three carried providers each
+ /// resolve to their own dialect, and every remaining member resolves to .
+ /// Enumerating for the reserved half is what makes a future enum
+ /// member fail here rather than silently browse as SQL Server.
+ ///
[Fact]
- public void DefaultDialectSelector_carriesSqlServerOnly()
+ public void DefaultDialectSelector_carriesSqlServerPostgresAndOracle()
{
SqlDriverBrowser.DefaultDialectSelector(SqlProvider.SqlServer).ShouldBeOfType();
+ SqlDriverBrowser.DefaultDialectSelector(SqlProvider.Postgres).ShouldBeOfType();
+ SqlDriverBrowser.DefaultDialectSelector(SqlProvider.Oracle).ShouldBeOfType();
- foreach (var provider in Enum.GetValues().Where(p => p != SqlProvider.SqlServer))
+ SqlProvider[] carried = [SqlProvider.SqlServer, SqlProvider.Postgres, SqlProvider.Oracle];
+ foreach (var provider in Enum.GetValues().Where(p => !carried.Contains(p)))
SqlDriverBrowser.DefaultDialectSelector(provider).ShouldBeNull();
}
@@ -178,6 +187,10 @@ public sealed class SqlDriverBrowserTests
// ---- provider availability ----
+ ///
+ /// The falsifiability control for the widened browse gate: is still
+ /// reserved and must be reported as unavailable, naming itself.
+ ///
[Fact]
public async Task Open_providerThisBuildDoesNotCarry_saysNotAvailableInThisBuild()
{
@@ -185,15 +198,15 @@ public sealed class SqlDriverBrowserTests
var ex = await Should.ThrowAsync(() =>
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");
}
///
/// 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.
///
[Fact]
public async Task Open_unavailableProviderAndUnresolvableRef_reportsTheProvider()
@@ -203,12 +216,34 @@ public sealed class SqlDriverBrowserTests
var ex = await Should.ThrowAsync(() =>
browser.OpenAsync(
- ConfigJson("Oracle", connectionStringRef: "MesStaging"), CancellationToken.None));
+ ConfigJson("Odbc", connectionStringRef: "MesStaging"), CancellationToken.None));
ex.Message.ShouldContain("not available in this build");
reads.ShouldBeEmpty();
}
+ ///
+ /// A carried provider must get past 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.
+ ///
+ [Theory]
+ [InlineData("Postgres")]
+ [InlineData("Oracle")]
+ public async Task Open_aCarriedProvider_passesTheGateAndResolvesTheRef(string provider)
+ {
+ var reads = new List();
+ var browser = new SqlDriverBrowser(environmentReader: name => { reads.Add(name); return null; });
+
+ var ex = await Should.ThrowAsync(() =>
+ browser.OpenAsync(
+ ConfigJson(provider, connectionStringRef: "MesStaging"), CancellationToken.None));
+
+ ex.Message.ShouldNotContain("not available in this build");
+ reads.ShouldContain(SqlDriverBrowser.EnvironmentVariableFor("MesStaging"));
+ }
+
// ---- malformed configuration ----
[Theory]
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverFactoryTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverFactoryTests.cs
index 0fb8fdb0..65d6a281 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverFactoryTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverFactoryTests.cs
@@ -122,6 +122,11 @@ public sealed class SqlDriverFactoryTests
// ---- provider gate ----
+ ///
+ /// The falsifiability control for the widened gate: 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.
+ ///
[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(() => 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");
}
+ ///
+ /// 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.
+ ///
+ [Fact]
+ public void CreateInstance_aReservedProvider_namesTheSupportedAndTheReservedSets()
+ {
+ var name = NewRef();
+ using var _ = new EnvironmentVariableScope(
+ SqlConnectionStringResolver.EnvironmentVariableFor(name), "Host=h;Database=db");
+
+ var ex = Should.Throw(() => 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));
+ }
+
+ ///
+ /// 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 SqlServerDialect would also construct without complaint and then speak
+ /// T-SQL at a Postgres server.
+ ///
+ [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(provider));
+ }
+
+ ///
+ /// 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.
+ ///
+ [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]