feat(sql): Odbc provider — ANSI SQL:2008 profile over System.Data.Odbc (lmxopcua#753)

SqlProvider.Odbc is now constructed; every enum member is a working backend.

- OdbcDialect: System.Data.Odbc 10.0.14 (no vulnerable packages), ANSI double-
  quote quoting, positional `?` markers, INFORMATION_SCHEMA catalog (system
  schemas of the common backends excluded by name — the live gate caught
  psqlODBC listing pg_catalog), FETCH FIRST n ROWS ONLY limits, a conservative
  1000-parameter chunk. Positional binding is safe because every caller adds
  parameters in marker order (planner IN list, catalog schema-then-table).
- Named queries on a positional provider are checked by `?` count against the
  declared params (literals/comments ignored), since there is no name to match.
- Supported backends are those whose SQL accepts that profile (PostgreSQL via
  psqlODBC proven, Snowflake-like); SQL Server/MySQL/DB2 should use their native
  provider. The host needs the ODBC driver manager + vendor driver.
- Reserved-provider controls in factory/probe/browser tests now use an
  undeclared ordinal (99); the AdminUI form offers Odbc and a test pins the
  dropdown theory to SqlDialects.Supported.
- Tests: OdbcDialectTests (unit, any host). OdbcReadTests + Docker/odbc/
  run-odbc-gate.sh run the built suite in a throwaway Linux SDK container with
  unixODBC + psqlODBC against a throwaway seeded PostgreSQL: KV IN-list
  positional binding, where-pair, topByTimestamp via FETCH FIRST, a two-
  parameter named query, and the browse walk. macOS has no vendor ODBC driver,
  so there the suite skips.

(lmxopcua#753)

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 17:13:47 -05:00
parent 63372e757a
commit 3c9ab4b668
16 changed files with 606 additions and 39 deletions
@@ -145,8 +145,9 @@ public sealed class SqlDriverFactoryTests
// ---- provider gate ----
/// <summary>
/// The falsifiability control for the widened gate: <see cref="SqlProvider.Odbc"/> is still a
/// reserved name and must fail loudly at construction, naming itself. Without this leg, a gate that
/// The falsifiability control for the widened gate: every declared member now constructs, so an
/// out-of-range ordinal (a config authored against a future enum) stands in for "a provider this build
/// does not carry" — it 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]
@@ -157,9 +158,9 @@ public sealed class SqlDriverFactoryTests
SqlConnectionStringResolver.EnvironmentVariableFor(name), "Host=h;Database=db");
var ex = Should.Throw<InvalidOperationException>(() => SqlDriverFactoryExtensions.CreateInstance(
"d1", $$"""{"provider":"Odbc","connectionStringRef":"{{name}}"}""", null));
"d1", $$"""{"provider":99,"connectionStringRef":"{{name}}"}""", null));
ex.Message.ShouldContain("Odbc");
ex.Message.ShouldContain("99");
ex.Message.ShouldContain("not available in this build");
}
@@ -175,9 +176,10 @@ public sealed class SqlDriverFactoryTests
SqlConnectionStringResolver.EnvironmentVariableFor(name), "Host=h;Database=db");
var ex = Should.Throw<InvalidOperationException>(() => SqlDriverFactoryExtensions.CreateInstance(
"d1", $$"""{"provider":"Odbc","connectionStringRef":"{{name}}"}""", null));
"d1", $$"""{"provider":99,"connectionStringRef":"{{name}}"}""", null));
ex.Message.ShouldContain(nameof(SqlProvider.SqlServer));
ex.Message.ShouldContain(nameof(SqlProvider.Odbc));
ex.Message.ShouldContain(nameof(SqlProvider.Postgres));
ex.Message.ShouldContain(nameof(SqlProvider.Oracle));
ex.Message.ShouldContain(nameof(SqlProvider.MySql));
@@ -194,6 +196,7 @@ public sealed class SqlDriverFactoryTests
[InlineData("Postgres", typeof(PostgresDialect))]
[InlineData("Oracle", typeof(OracleDialect))]
[InlineData("MySql", typeof(MySqlDialect))]
[InlineData("Odbc", typeof(OdbcDialect))]
public void ParseBinding_aSupportedProvider_bindsThatProvidersDialect(string provider, Type expected)
{
var name = NewRef();
@@ -215,6 +218,7 @@ public sealed class SqlDriverFactoryTests
[InlineData("Postgres")]
[InlineData("Oracle")]
[InlineData("MySql")]
[InlineData("Odbc")]
public async Task CreateInstance_aNewlySupportedProvider_constructsPastTheProviderGate(string provider)
{
var name = NewRef();