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

11 KiB
Raw Blame History

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 PackageReferences (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.