# Sql Postgres + Oracle providers Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development > (opus implementers per user instruction). **Goal:** Make `SqlProvider.Postgres` and `SqlProvider.Oracle` working backends via managed ADO.NET providers (Npgsql, Oracle.ManagedDataAccess.Core). **Architecture:** See `docs/plans/2026-08-05-sql-postgres-oracle-design.md` (this branch). Two new dialects in the existing `.Dialect` project; dialect-owned parameter markers/bind-names + `ConfigureCommand` (Oracle BindByName); provider gates widened; AdminUI dropdown; env-gated docker integration suites. ~~Chrome/AdminUI live gate is DEFERRED to the operator.~~ **Update 2026-08-07 (truth sweep): the chrome/AdminUI gate RAN and PASSED 2026-08-05** — and found a real defect (Test Connect was SqlServer-gated for both new providers), fixed in merge `4e461951`; see `2026-08-05-sql-postgres-oracle-design.md` §4.3 record. **Tech Stack:** .NET 10, central package management (`Directory.Packages.props`), xUnit v3 + Shouldly, SQLite falsifiability control. **Worktree:** `/Users/dohertj2/Desktop/OtOpcUa/.claude/worktrees/sql-multi-provider` (branch `feature/sql-multi-provider`). All paths below relative to it. --- ### Task 0: Package pins **Classification:** trivial **Estimated implement time:** ~3 min **Parallelizable with:** Task 1, Task 5 **Files:** - Modify: `Directory.Packages.props` - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect.csproj` Add `PackageVersion` entries for `Npgsql` (alphabetical slot after `MQTTnet`) and `Oracle.ManagedDataAccess.Core` (after the `OPCFoundation.*` block) — latest stable on the feed (`dotnet package search --exact-match` to confirm; Npgsql 9.x line, ODP.NET Core 23.x line; both must target net8.0+). Add both `PackageReference`s (version-less) to the `.Dialect` csproj under the existing `Microsoft.Data.SqlClient` reference, extending that ItemGroup's rationale comment. `dotnet restore ZB.MOM.WW.OtOpcUa.slnx` must come back clean WITH the audit active — if NU1903 fires on a transitive dep, apply the surgical direct-pin pattern documented at `Directory.Packages.props:118-145`, never a suppress. Commit `build(sql): pin Npgsql + Oracle.ManagedDataAccess.Core (managed providers only)`. ### Task 1: Dialect-owned parameter naming + `ConfigureCommand` **Classification:** high-risk **Estimated implement time:** ~5 min **Parallelizable with:** Task 0, Task 5 **Files:** - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/ISqlDialect.cs` - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/SqlServerDialect.cs` - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlGroupPlanner.cs` (:21-25 consts, :143, :151, :188) - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlQueryPlan.cs` (bind names alongside markers if the plan record needs both) - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlPollReader.cs` (:379-405 — ConfigureCommand call + bind-name copy) - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlCatalogLoader.cs` (:97-113, `Bind` :181-188) - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverProbe.cs` (ConfigureCommand) - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser/SqlBrowseSession.cs` (ConfigureCommand) - Modify: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqliteDialect.cs` (implement new members; keep linked copy in Browser.Tests compiling) - Test: `SqlGroupPlannerTests.cs`, `SqlPollReaderTests.cs`, `SqlCatalogGateDriverTests.cs` (+ a colon-marker fake dialect in the test tree) Per design §2 bullet 2. `ISqlDialect` gains `ParameterMarker(string baseName)`, `ParameterBindName(string baseName)`, `ConfigureCommand(DbCommand)`; SqlServerDialect returns `"@"+name` / `"@"+name` / no-op — **behaviour today must be byte-identical** (existing planner SQL-text assertions must pass untouched; if any needs editing, that is a regression, stop). Planner takes markers from its dialect; catalog SQL text and loader bind names route through the dialect so the two can never disagree. TDD: the colon-marker fake proves markers flow (observed red against the `@k` consts first). Every `CreateCommand` site calls `ConfigureCommand` immediately after creation. Commit `refactor(sql): dialect-owned parameter markers, bind names and command configuration`. ### Task 2: `PostgresDialect` **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Task 3, Task 5 **Files:** - Create: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/PostgresDialect.cs` - Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/PostgresDialectTests.cs` (new) Per design §2 Postgres bullet. `Factory => NpgsqlFactory.Instance`; `Provider => SqlProvider.Postgres`; `@` markers/bind names; `"`-quoting `""`-escaped, MaxIdentifierLength 63, same control-char/Unicode-Format rejection discipline as `SqlServerDialect.QuoteIdentifier` (:88-114); `LivenessSql "SELECT 1"`; limit `""`/`" LIMIT 1"`; catalog SQL over `information_schema.schemata/tables/columns` excluding `pg_catalog` + `information_schema`, `DefaultSchemaSql "SELECT current_schema()"` (parameter markers in catalog SQL via the Task 1 members); `MapColumnType` per design (unknown → the same fallback SqlServerDialect uses — read it first). Tests mirror `SqlServerDialectTests` structure: quoting theory, SQL text, type-map theory, marker members. Commit `feat(sql): PostgresDialect (Npgsql)`. ### Task 3: `OracleDialect` **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Task 2, Task 5 **Files:** - Create: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/OracleDialect.cs` - Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/OracleDialectTests.cs` (new) Per design §2 Oracle bullet. `OracleClientFactory.Instance`; `:` markers, **bare** bind names; `ConfigureCommand` casts to `OracleCommand` and sets `BindByName = true` (the test constructs a real `OracleCommand` and asserts the flag — this is the load-bearing test of the whole branch; also assert a foreign `DbCommand` passes through untouched). Quoting `"`/`""`, max 128; `LivenessSql "SELECT 1 FROM DUAL"`; limit `""`/`" FETCH FIRST 1 ROWS ONLY"`; catalog SQL: `ALL_USERS` schemas, `ALL_TABLES` ∪ `ALL_VIEWS` tables, `ALL_TAB_COLUMNS` columns, `sys_context('userenv','current_schema')` default schema — markers via `:schema`/`:table` through the Task 1 members; `MapColumnType`: `NUMBER`→Float64 (doc the DeclaredType escape hatch in the XML doc comment), `BINARY_DOUBLE`/`BINARY_FLOAT`, `VARCHAR2`/`NVARCHAR2`/`CHAR`/`NCHAR`/`CLOB`, `DATE`/`TIMESTAMP*`, `RAW`, plus `FLOAT`/`INTEGER` synonyms. Commit `feat(sql): OracleDialect (ODP.NET managed, BindByName)`. ### Task 4: Widen the provider gates **Classification:** small **Estimated implement time:** ~4 min **Parallelizable with:** none **Files:** - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverFactoryExtensions.cs` (:208-215) - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser/SqlDriverBrowser.cs` (:126-127, :146-151 comment) - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts/SqlProvider.cs` (doc comments) - Test: `SqlDriverFactoryTests.cs` (:133-135 → MySql as rejected control; new: Postgres/Oracle construct with their dialects), `SqlDriverBrowserTests.cs` (:188-196 same flip) TDD: red first — author `{"provider":"Postgres"}` and assert construction reaches the connection-string resolver (or dialect type assertion via the binding) instead of the not-available throw. Rejection message updated to name the three supported providers. Commit `feat(sql): construct Postgres and Oracle providers; MySql/Odbc stay reserved`. ### Task 5: AdminUI dropdown + docs **Classification:** small **Estimated implement time:** ~4 min **Parallelizable with:** Task 1, Task 2, Task 3 **Files:** - Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/SqlDriverForm.razor` (:18-25) - Modify: `docs/drivers/Sql.md` (:15-18, :96 provider row, browse section) - Test: `tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/` — extend the existing Sql driver-form model test file (find `SqlDriverFormModelTests` or nearest equivalent) with round-trip of `provider: Postgres` / `Oracle` Dropdown gains `Postgres` + `Oracle` options; helper text → "SQL Server, PostgreSQL and Oracle are supported; MySql/Odbc are reserved." Sql.md rewritten per design §5 including the Oracle `NUMBER`→Float64 note and the 12.2+ floor. Commit `feat(sql-adminui): Postgres + Oracle in the provider dropdown; docs`. ### Task 6: Integration fixtures + env-gated suites **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** none **Files:** - Create: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.IntegrationTests/Docker/postgres/docker-compose.yml` + `seed-pg.sql` - Create: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.IntegrationTests/Docker/oracle/docker-compose.yml` + `seed-oracle.sql` - Create: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.IntegrationTests/PostgresReadTests.cs`, `OracleReadTests.cs` Per design §4.2. Compose shapes mirror the existing mssql fixture (`Docker/docker-compose.yml`: project label `project: lmxopcua`, one-shot seed service, healthcheck): `postgres:17-alpine` port `15432:5432` env `POSTGRES_PASSWORD=${SQL_PG_PASSWORD}`; `gvenzl/oracle-free:23-slim` port `11521:1521` env `ORACLE_PASSWORD=${SQL_ORACLE_PASSWORD}` (gvenzl images take `APP_USER`/ `APP_USER_PASSWORD` for a non-SYS user — use `otopcua`/the password env). Seeds create one KeyValue table + one WideRow table with a timestamp column, plus (Oracle) a deliberately UPPER-CASE table authored lower-case in the test to pin catalog case-folding. Test classes follow `SqlServerReadTests`/`SqlBlackholeTimeoutTests` conventions exactly: env vars `SQL_PG_ENDPOINT`/`SQL_PG_USER`/`SQL_PG_PASSWORD`, `SQL_ORACLE_ENDPOINT`/`SQL_ORACLE_USER`/ `SQL_ORACLE_PASSWORD`; `Assert.SkipWhen` absent; connection strings via each provider's builder obtained FROM THE FACTORY (`dialect.Factory.CreateConnectionStringBuilder()`) so the csproj keeps its no-direct-provider-reference discipline (`.IntegrationTests.csproj:29-33`). Legs per suite: initialize (liveness + catalog), KeyValue read, WideRow topByTimestamp read, browse-session walk, case-folding leg. Commit `test(sql): env-gated Postgres + Oracle integration suites`. ### Task 7: Full-suite verification + bookkeeping **Classification:** small **Estimated implement time:** ~5 min **Parallelizable with:** none `dotnet build ZB.MOM.WW.OtOpcUa.slnx` (0 errors), full Sql unit + Browser.Tests + AdminUI.Tests + Configuration.Tests (the SqlCredentialGuard suites must stay green), integration suites compile + skip clean offline. Update `docs/plans/2026-07-24-driver-expansion-tracking.md` Sql row/deferral note. Commit. ### Task 8: Fixture deploy + live integration run (main session) **Classification:** standard **Estimated implement time:** ~15 min (main session, after merge) Main session, not a subagent: rsync the two fixture dirs to `dohertj2@10.100.0.35:/opt/otopcua-sql-pg/` and `/opt/otopcua-sql-oracle/`, `docker compose up -d` each over ssh, then run `dotnet test --filter "FullyQualifiedName~PostgresRead|FullyQualifiedName~OracleRead"` with the env vars set. (oracle-free first pull is ~1.5 GB — allow time; if the host can't take it, record the suite as authored-but-not-yet-run.) Record results in the design doc §Outcome. **Chrome/AdminUI live gate — DEFERRED to the operator.** After merge the operator runs, in a chrome-capable session: `/superpowers-extended-cc:executing-plans docs/plans/2026-08-05-sql-postgres-oracle.md` — gate section: provider dropdown authoring, Test-connect, `/raw` browse + browse-commit against both fixtures at `http://localhost:9200`.