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