Files
Joseph Doherty f5c30ed0ef
v2-ci / unit-tests (push) Has been cancelled
feat(sql): authored write block — Update/Insert/Upsert/Procedure + timestamp stamp; ODBC scope + login docs (lmxopcua#781)
Item 2: a tag's optional `write` block picks the statement the driver builds from its own
catalog-validated identifiers (no authored SQL; values always bound):
- Update (default, unchanged back-compat UPDATE ... WHERE key; 0 rows = BadNoData)
- Insert (key+value[+stamp]; append for a WideRow topByTimestamp tag)
- Upsert: SQL Server MERGE WITH (HOLDLOCK), PostgreSQL INSERT ... ON CONFLICT, MySQL
  ON DUPLICATE KEY UPDATE (1-or-2 rows), Oracle MERGE from DUAL
- Procedure: named stored procedure resolved against the routine catalog by the catalog gate
  (INFORMATION_SCHEMA.ROUTINES / ALL_PROCEDURES), value/key/timestamp params by name (MySQL positional)
- stampTimestamp sets the tag's timestamp column to the write time (UTC)
Odbc serves Update/Insert only (shared SqlWriteSupport table). Constraint failures map to
BadEntryExists (unique) / BadOutOfRange (other integrity, data exception); everything else,
permission denied included, stays BadCommunicationError. WriteIdempotent stays false.
The parser refuses malformed blocks (strict keys/enums/param charset); DraftValidator emits
blocking SqlWriteBlockInvalid / SqlWriteModeUnsupported from the driver's own parse; the AdminUI
editor surfaces the same message.

Items 1 + 4 (docs/drivers/Sql.md): ODBC stays the ANSI SQL:2008 profile (native providers for
SQL Server/MySQL, DB2 unsupported) with an ops note on driver manager + vendor driver on every
Odbc and AdminUI host; read-only drivers require a SELECT-only login, writable drivers a login
limited to their write targets.

Tests: SqlWriteModeTests (parse, every dialect's SQL, gate, status map, SQLite end-to-end),
DraftValidatorSqlWriteBlockTests, AdminUI round-trip; live SqlWriteModesLiveTests on SQL Server,
PostgreSQL, MySQL, Oracle (seeds gain write_targets + set_target) and an Odbc write leg.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 22:59:09 -05:00

113 lines
4.6 KiB
SQL

-- Seed for the Sql driver's Oracle live gate (OracleReadTests), applied once by the compose
-- `oracle-seed` one-shot AS THE APPLICATION USER (otopcua) against the OWNED `otopcua-sql-oracle`
-- container. Oracle equates a schema with a user, so everything below lands in the OTOPCUA schema.
--
-- The table shapes mirror the SQL Server fixture (SqlPollServerFixture) row for row — a present
-- value, a present row with a NULL cell, and a wide-row table whose NEWEST row is deliberately not
-- its first — so the same assertions can be made against both backends and a divergence between
-- the two provider paths shows up as a failing assertion rather than as two suites that quietly
-- test different things.
--
-- ⚠️ EVERY IDENTIFIER HERE IS UNQUOTED, AND THAT IS THE POINT. Oracle upper-cases an unquoted
-- identifier, so the catalog stores OTOPCUA / TAG_VALUES / TAG_NAME while an operator authoring a
-- tag in the AdminUI naturally types lower case. The suite's case-folding leg authors the lower-case
-- spelling and expects `SqlCatalogGate` to resolve it through `SqlCatalog.TryMatch` (exact-ordinal
-- first, then a UNIQUE case-insensitive match) and substitute the catalog's own UPPER-case spelling
-- before `OracleDialect.QuoteIdentifier` double-quotes it — which is load-bearing, because a quoted
-- "tag_values" and a quoted "TAG_VALUES" are DIFFERENT objects to Oracle. Quoting a name here would
-- make that leg meaningless.
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET FEEDBACK OFF
SET DEFINE OFF
-- Drop-and-recreate rather than merge-or-insert: the seed is an assertion contract, and a leftover
-- row from an older revision of this file would make a passing test meaningless. ORA-00942 (table
-- does not exist) is the expected outcome on a first run and is the only error swallowed.
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE tag_values PURGE';
EXCEPTION
WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE latest_status PURGE';
EXCEPTION
WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF;
END;
/
-- Key-value (EAV) source: tag_name → num_value, with a source timestamp.
-- num_value is NUMBER, Oracle's one numeric family — which OracleDialect.MapColumnType folds to
-- Float64 (the documented under-specification; see that method's remarks).
CREATE TABLE tag_values (
tag_name VARCHAR2(128) NOT NULL,
num_value NUMBER,
sample_ts TIMESTAMP(3) NOT NULL
);
INSERT INTO tag_values (tag_name, num_value, sample_ts)
VALUES ('Line1.Speed', 42, TIMESTAMP '2026-07-24 10:00:00');
INSERT INTO tag_values (tag_name, num_value, sample_ts)
VALUES ('Line1.Temp', NULL, TIMESTAMP '2026-07-24 10:00:01');
INSERT INTO tag_values (tag_name, num_value, sample_ts)
VALUES ('Line1.Pressure', 3.5, TIMESTAMP '2026-07-24 10:00:02');
-- Wide-row source: one row per station, several value columns. Station 8 is inserted LAST and is
-- also the newest sample, while station 7 is first — so a topByTimestamp plan that lost its
-- ORDER BY … DESC (or its FETCH FIRST 1 ROWS ONLY) returns a different, wrong value rather than a
-- coincidentally right one.
CREATE TABLE latest_status (
station_id NUMBER(10) NOT NULL,
oven_temp NUMBER,
pressure NUMBER,
sample_ts TIMESTAMP(3) NOT NULL
);
INSERT INTO latest_status (station_id, oven_temp, pressure, sample_ts)
VALUES (7, 180.5, 1.2, TIMESTAMP '2026-07-24 10:00:00');
INSERT INTO latest_status (station_id, oven_temp, pressure, sample_ts)
VALUES (9, NULL, 1.6, TIMESTAMP '2026-07-24 10:00:01');
INSERT INTO latest_status (station_id, oven_temp, pressure, sample_ts)
VALUES (8, 210.25, 1.4, TIMESTAMP '2026-07-24 10:00:02');
-- Write-mode targets (lmxopcua#781, SqlWriteModesLiveTests): a primary-keyed setpoint table for Update / Insert /
-- MERGE, and a procedure that journals its arguments (named notation: P_VALUE / P_KEY / P_TS).
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE write_targets PURGE';
EXCEPTION
WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE proc_calls PURGE';
EXCEPTION
WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF;
END;
/
CREATE TABLE write_targets (
tag_name VARCHAR2(128) NOT NULL PRIMARY KEY,
sp_value NUMBER NOT NULL,
stamp_ts TIMESTAMP(3)
);
INSERT INTO write_targets (tag_name, sp_value, stamp_ts) VALUES ('Present', 1, NULL);
CREATE TABLE proc_calls (
tag_name VARCHAR2(128) NOT NULL,
sp_value NUMBER,
stamp_ts TIMESTAMP(3)
);
CREATE OR REPLACE PROCEDURE set_target(p_value IN NUMBER, p_key IN VARCHAR2, p_ts IN TIMESTAMP) AS
BEGIN
INSERT INTO proc_calls (tag_name, sp_value, stamp_ts) VALUES (p_key, p_value, p_ts);
END;
/
COMMIT;
EXIT;