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
+19 -5
View File
@@ -15,11 +15,11 @@ An ordinary SQL database — typically a MES / LIMS / historian staging table th
already writes to. There is no protocol here and nothing to "connect" to a machine: the driver runs
`SELECT`s on a timer and turns cells into tag values.
**Four providers construct**: `SqlServer` (`Microsoft.Data.SqlClient`), `Postgres`
**All five providers construct**: `SqlServer` (`Microsoft.Data.SqlClient`), `Postgres`
(`Npgsql`), `Oracle` (ODP.NET managed — **Oracle 12.2 or later**, the floor the
`FETCH FIRST` paging needs) and `MySql` (`MySqlConnector`, MIT, fully managed — MySQL 8 /
MariaDB). `SqlProvider` also declares `Odbc`, which remains a **reserved name, not a working
backend** — authoring it fails the driver rather than silently falling back. The provider → dialect
`FETCH FIRST` paging needs), `MySql` (`MySqlConnector`, MIT, fully managed — MySQL 8 /
MariaDB) and `Odbc` (`System.Data.Odbc`, see below). An undeclared provider value fails the driver
rather than silently falling back. The provider → dialect
map is one shared table (`SqlDialects`) that the factory, the Test Connect probe and the browse
picker all read, so the three can no longer disagree about what a build supports.
@@ -31,6 +31,20 @@ boolean under MySqlConnector's default `TreatTinyAsBoolean`; `BIT(n)`, `TIME`, `
types infer as `String`. Live gate: `Docker/mysql/docker-compose.yml` + `MySqlReadTests`
(`SQL_MYSQL_ENDPOINT` / `SQL_MYSQL_PASSWORD`).
ODBC specifics: `OdbcDialect` is the **ANSI SQL:2008 profile** — double-quoted identifiers,
positional `?` parameters, the standard `INFORMATION_SCHEMA` views and `FETCH FIRST n ROWS ONLY`.
ODBC abstracts the wire, not the SQL, so the backend behind the DSN must accept all four:
PostgreSQL (psqlODBC — the live gate), Snowflake and similar do; **SQL Server** (needs `OFFSET …
FETCH` after an `ORDER BY`), **MySQL** (no `FETCH FIRST`) and **DB2** (no `INFORMATION_SCHEMA`) do
not and should use their native provider. It is also the one provider that is **not fully
managed**: every host running an Odbc instance needs the platform driver manager (unixODBC / iODBC
/ Windows' own) and the vendor's ODBC driver — on the driver nodes and, for browse, the AdminUI
nodes. Parameters bind in order, so the planner, the catalog loader and the browse session all add
them in marker order, and a named query's `params` bind in authored order (the factory refuses a
statement whose `?` count differs). Live gate: `Docker/odbc/run-odbc-gate.sh` runs `OdbcReadTests`
in a throwaway Linux container with unixODBC + psqlODBC against a throwaway PostgreSQL; on macOS
the suite is unit-tested only (`OdbcDialectTests`) because no vendor ODBC driver is installed.
Two provider-specific things are worth knowing before you author against them:
- **Identifier case folds, and the catalog gate absorbs it.** Postgres lower-cases and Oracle
@@ -195,7 +209,7 @@ existed in the parser — nothing reads it.)
| Key | Default | Notes |
|---|---|---|
| `provider` | `SqlServer` | `SqlServer`, `Postgres`, `Oracle` or `MySql`. `Odbc` is reserved and rejected. |
| `provider` | `SqlServer` | `SqlServer`, `Postgres`, `Oracle`, `MySql` or `Odbc`. |
| `connectionStringRef` | — | **A name, never a connection string.** See below. |
| `defaultPollInterval` | `00:00:05` | Applied to any group without its own interval. |
| `operationTimeout` | `00:00:15` | Client-side wall-clock deadline; a breach surfaces `BadTimeout`. |