2331f7ed14
v2-ci / unit-tests (push) Waiting to run
allowWrites is no longer inert: SqlDriver implements IWritable, default off. - Gates (outermost first): node Writable + WriteOperate (server), driver allowWrites, the tag's own TagConfig "writable": true (strict JSON boolean; anything else refuses the tag, and the deploy gate says why), and a model that names exactly one row — KeyValue, or WideRow by where-pair. topByTimestamp and Query are BadNotWritable. - SqlWritePlanner: the only statement a write can become is UPDATE <table> SET <value col> = @v WHERE <key col> = @k, built from the catalog-gate-canonical identifiers (dialect-quoted) with value and key bound; value-before-key marker order so positional ODBC binds correctly. No authored write SQL, INSERT, UPSERT or procedure. - Executed in a transaction that commits only when exactly one row was touched: 0 rows -> BadNoData (never inserts), >1 -> rolled back, BadConfigurationError, unknown count -> rolled back, BadNotSupported. Bounded by operationTimeout; provider errors -> BadCommunicationError; health untouched. - Discovery advertises Operate only where a write could succeed. - AdminUI: driver form's Allow writes switch is live; tag editor gains a per-tag write switch; model validates writable strictly (parity corpus). - Factory no longer warns "allowWrites ignored"; it logs whether writes are on. - Tests: SqlWriteTests (planner SQL/bind order/injection/coercion, every gate, readback, absent key, two-row rollback, gate-rejected tag, discovery); MySqlReadTests write legs against InnoDB (keyed row only, two-row rollback, server-side grant refusal, default-off). Seed adds a setpoints table and the one UPDATE grant. (lmxopcua#753) Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
88 lines
4.2 KiB
SQL
88 lines
4.2 KiB
SQL
-- Seed for the Sql driver's MySQL live gate (MySqlReadTests, lmxopcua#753), applied once by the compose
|
|
-- `mysql-seed` one-shot as root against the OWNED `otopcua-sql-mysql` container. `__APP_PASSWORD__` is
|
|
-- substituted by the one-shot before this reaches the server.
|
|
--
|
|
-- Table shapes mirror the SQL Server and PostgreSQL fixtures row for row, so the same assertions hold on all
|
|
-- backends. Two things are load-bearing:
|
|
--
|
|
-- 1. The suite connects as `otopcua`, which holds SELECT on `sqlpoll` and nothing else (plus EXECUTE on the
|
|
-- side-effect function and UPDATE on the one write-target table, below). A MySQL "schema" is a database,
|
|
-- and information_schema shows a principal only the objects it holds a privilege on — so MySqlDialect.ListSchemasSql returning `sqlpoll` for this
|
|
-- login is the real provisioning check, not a superuser's view.
|
|
-- 2. Every identifier is lower case. The suite authors the table name in UPPER case and expects the catalog
|
|
-- gate to resolve it through a unique case-insensitive match: on Linux, MySQL table names are case-
|
|
-- SENSITIVE, so without the gate's substitution the quoted upper-case name would name no table.
|
|
|
|
DROP DATABASE IF EXISTS sqlpoll;
|
|
CREATE DATABASE sqlpoll;
|
|
USE sqlpoll;
|
|
|
|
-- Key-value (EAV) source.
|
|
CREATE TABLE tag_values (
|
|
tag_name varchar(128) NOT NULL,
|
|
num_value double NULL,
|
|
sample_ts datetime(3) NOT NULL
|
|
);
|
|
INSERT INTO tag_values (tag_name, num_value, sample_ts) VALUES
|
|
('Line1.Speed', 42.0, '2026-07-24 10:00:00'),
|
|
('Line1.Temp', NULL, '2026-07-24 10:00:01'),
|
|
('Line1.Pressure', 3.5, '2026-07-24 10:00:02');
|
|
|
|
-- Wide-row source. Station 8 is inserted LAST and is the newest sample; station 7 is first — so a plan that
|
|
-- lost its ORDER BY … DESC or its LIMIT 1 returns a different, wrong value.
|
|
CREATE TABLE latest_status (
|
|
station_id int NOT NULL,
|
|
oven_temp double NULL,
|
|
pressure double NULL,
|
|
sample_ts datetime(3) NOT NULL
|
|
);
|
|
INSERT INTO latest_status (station_id, oven_temp, pressure, sample_ts) VALUES
|
|
(7, 180.5, 1.2, '2026-07-24 10:00:00'),
|
|
(9, NULL, 1.6, '2026-07-24 10:00:01'),
|
|
(8, 210.25, 1.4, '2026-07-24 10:00:02');
|
|
|
|
-- Integer-keyed EAV source: an authored text keyValue must compare against an INT key column.
|
|
CREATE TABLE int_tag_values (
|
|
tag_id int NOT NULL,
|
|
num_value double NULL,
|
|
sample_ts datetime(3) NOT NULL
|
|
);
|
|
INSERT INTO int_tag_values (tag_id, num_value, sample_ts) VALUES
|
|
(101, 77.5, '2026-07-24 10:00:00'),
|
|
(102, 12.0, '2026-07-24 10:00:01');
|
|
|
|
-- Named-query rollback probe: bump_counter() WRITES a row and returns 1. Called through a SELECT, it passes the
|
|
-- read-only guard (nothing lexical says "write"); the driver's always-rolled-back transaction is what must
|
|
-- discard the row. InnoDB, so the insert is transactional. SQL SECURITY DEFINER (root), so `otopcua` needs only
|
|
-- EXECUTE — it holds no INSERT of its own.
|
|
CREATE TABLE side_effects (n int NOT NULL) ENGINE = InnoDB;
|
|
DELIMITER //
|
|
CREATE FUNCTION bump_counter() RETURNS int
|
|
NOT DETERMINISTIC MODIFIES SQL DATA SQL SECURITY DEFINER
|
|
BEGIN
|
|
INSERT INTO side_effects (n) VALUES (1);
|
|
RETURN 1;
|
|
END//
|
|
DELIMITER ;
|
|
|
|
-- Write target (lmxopcua#753): the ONLY table the suite login may UPDATE. `dup` deliberately holds two rows
|
|
-- for one key, so a write keyed on it matches two rows and must be rolled back. tag_values stays SELECT-only,
|
|
-- so a write tag pointed there proves the server's own grant refuses it.
|
|
CREATE TABLE setpoints (
|
|
tag_name varchar(128) NOT NULL,
|
|
sp_value double NULL
|
|
) ENGINE = InnoDB;
|
|
INSERT INTO setpoints (tag_name, sp_value) VALUES
|
|
('Line1.SpeedSp', 10.0),
|
|
('Line1.TempSp', 20.0),
|
|
('dup', 1.0),
|
|
('dup', 2.0);
|
|
|
|
-- ── the read-only login ──────────────────────────────────────────────────────────────────────
|
|
DROP USER IF EXISTS 'otopcua'@'%';
|
|
CREATE USER 'otopcua'@'%' IDENTIFIED BY '__APP_PASSWORD__';
|
|
GRANT SELECT ON sqlpoll.* TO 'otopcua'@'%';
|
|
GRANT EXECUTE ON FUNCTION sqlpoll.bump_counter TO 'otopcua'@'%';
|
|
GRANT UPDATE ON sqlpoll.setpoints TO 'otopcua'@'%';
|
|
FLUSH PRIVILEGES;
|