Files
lmxopcua/docs/plans/2026-08-05-sql-postgres-oracle.md
T

211 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
**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 <id> --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`.