fix(localdb): refuse AUTOINCREMENT keys at RegisterReplicated (LocalDb-005)

Each node assigns its own AUTOINCREMENT keys, so two different rows inserted
on the two nodes share a pk_json and LWW silently keeps one on both nodes.
RegisterReplicated now refuses such a table (detected from SQLite column
metadata, before any trigger is installed). A plain INTEGER PRIMARY KEY is
still accepted (application-assigned ids are legitimate); the requirement
that replicated keys be unique across both nodes is documented in the
RegisterReplicated remarks and README Limitations. No ScadaBridge or OtOpcUa
replicated table is AUTOINCREMENT.

(scadaproj#46)

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 15:09:45 -05:00
parent 76e9c355b4
commit fd46999946
5 changed files with 142 additions and 6 deletions
+24 -3
View File
@@ -10,7 +10,7 @@
| Last reviewed | 2026-11-09 |
| Reviewer | Claude |
| Commit reviewed | `91fb2b4` |
| Open findings | 2 |
| Open findings | 1 |
## Summary
@@ -362,7 +362,7 @@ applied and that no dead-letter is written. README §Semantics is updated.
| Severity | Low |
| Priority | P3 |
| Category | Documentation & XML docs |
| Status | Open |
| Status | Resolved |
| Location | `ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Internal/SqliteLocalDb.cs:197` |
**Description**
@@ -393,7 +393,28 @@ at registration when the sole PK column is an `INTEGER PRIMARY KEY` rowid alias.
**Resolution**
_Unresolved._
Resolved 2026-11-09 (scadaproj#46, LocalDb `0.3.6`, unpublished). Re-verified with a throwaway 2-node probe:
`ev(id INTEGER PRIMARY KEY AUTOINCREMENT, msg TEXT)`, `'from A'` inserted on A and `'from B'` on B, both nodes ended
with `1:from B` only and no dead-letter. Fix, taking the review's safer option: `RegisterReplicated` (both overloads)
now **refuses** a table whose primary key is `AUTOINCREMENT` with a `LocalDbRegistrationException` that names the
column and the remedy, before any trigger is installed (`SqliteLocalDb.EnsureNoAutoincrementKey`). Detection reads
SQLite's column metadata (`IsAutoIncrement` from `sqlite3_table_column_metadata`), not the `CREATE` text. A refusal
rather than a warning, because `AUTOINCREMENT` only exists to have SQLite assign the key, which is never safe on a
replicated table, and `SqliteLocalDb` has no logger to warn through.
A plain `INTEGER PRIMARY KEY` is still accepted, because an application may assign it from a node-scoped range;
refusing it would break schemas that are correct today. Its hazard when left `NULL` is documented instead: the
`RegisterReplicated` XML remarks and README §Limitations now say replicated primary keys must be unique across both
nodes and recommend writer-minted TEXT GUIDs, and the quickstart notes it.
Compatibility: checked read-only on 2026-11-09. ScadaBridge's 12 replicated tables (`SiteLocalDbSetup.ReplicatedTables`)
and OtOpcUa's 5 (`LocalDbSetup.ReplicatedTables`: the two deployment-cache tables, the alarm S&F events table, and
the alarm condition state and comments tables) all use TEXT or composite keys, and none is `AUTOINCREMENT`. Both apps had already replaced their historical
`INTEGER PRIMARY KEY AUTOINCREMENT` keys with GUIDs for this reason, so neither boot is affected.
Tests (`RegistrationTests`): `Register_AutoincrementKey_Throws` and `..._WithBaselineToo` fail pre-fix. Two guards pass
both ways: a plain `INTEGER PRIMARY KEY` still registers, and a column named `autoincrement` with a `DEFAULT
'AUTOINCREMENT'` is not mistaken for the keyword.
### LocalDb-006 — Column names containing `'` are captured but break the apply and snapshot SQL