From e36061dfd4bee8ec1c6d855f81b63fff51ea1584 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 14 May 2026 15:42:21 +0100 Subject: [PATCH 1/2] fix(codegen): temporal current-version UNIQUE; valid_to CHECK Closes #41. `verisimdb_temporal_versions` had a non-unique partial index on `(entity_id, table_name) WHERE valid_to IS NULL`. Two concurrent writers could both successfully insert a row with `valid_to=NULL` for the same entity, leaving two "current" versions and breaking the point-in-time query semantics. There was also no constraint that `valid_to` (when set) couldn't precede `valid_from`. - Promote the partial index to `CREATE UNIQUE INDEX` so the storage layer enforces "at most one current version per (entity, table)". - Add `CHECK (valid_to IS NULL OR valid_to >= valid_from)` so a version interval can't be backwards. Test `test_temporal_table_has_unique_partial_index_and_valid_to_check` asserts both clauses appear in the emitted DDL with temporal enabled. `cargo clippy --all-targets -- -D warnings` clean; 37 unit tests pass. Co-Authored-By: Claude Opus 4.7 --- src/codegen/overlay.rs | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/codegen/overlay.rs b/src/codegen/overlay.rs index 20e6612..7dd4a29 100644 --- a/src/codegen/overlay.rs +++ b/src/codegen/overlay.rs @@ -168,6 +168,10 @@ fn generate_lineage_table() -> String { /// point-in-time queries and rollback. Each version records when it /// became active (`valid_from`) and when it was superseded (`valid_to`). fn generate_temporal_table() -> String { + // The partial UNIQUE index enforces "at most one current version per + // (entity, table)" at the storage layer — two concurrent writers can no + // longer both insert a row with `valid_to IS NULL` for the same entity. + // The CHECK ensures valid_to never precedes valid_from. Closes #41. "-- Temporal: version history with point-in-time support\n\ CREATE TABLE IF NOT EXISTS verisimdb_temporal_versions (\n\ \x20 entity_id TEXT NOT NULL,\n\ @@ -177,9 +181,10 @@ fn generate_temporal_table() -> String { \x20 valid_to TEXT, -- ISO 8601, NULL if current\n\ \x20 snapshot TEXT NOT NULL, -- JSON serialisation of entity state\n\ \x20 operation TEXT NOT NULL, -- insert, update, rollback\n\ - \x20 PRIMARY KEY (entity_id, table_name, version)\n\ + \x20 PRIMARY KEY (entity_id, table_name, version),\n\ + \x20 CHECK (valid_to IS NULL OR valid_to >= valid_from)\n\ );\n\ - CREATE INDEX IF NOT EXISTS idx_temporal_current ON verisimdb_temporal_versions(entity_id, table_name) WHERE valid_to IS NULL;\n\n" + CREATE UNIQUE INDEX IF NOT EXISTS idx_temporal_current ON verisimdb_temporal_versions(entity_id, table_name) WHERE valid_to IS NULL;\n\n" .to_string() } @@ -331,6 +336,35 @@ mod tests { ); } + /// The "current version" partial index must be UNIQUE and the + /// `valid_to >= valid_from` CHECK must be present (closes #41). + /// Two concurrent writers must not be able to leave two rows with + /// `valid_to IS NULL` for the same `(entity_id, table_name)`. + #[test] + fn test_temporal_table_has_unique_partial_index_and_valid_to_check() { + let schema = test_schema(); + let octad = OctadConfig { + enable_provenance: false, + enable_lineage: false, + enable_temporal: true, + enable_access_control: false, + enable_constraints: false, + enable_simulation: false, + }; + let ddl = generate_sidecar_schema(&schema, &octad); + assert!(ddl.contains("verisimdb_temporal_versions")); + assert!( + ddl.contains( + "CREATE UNIQUE INDEX IF NOT EXISTS idx_temporal_current ON verisimdb_temporal_versions(entity_id, table_name) WHERE valid_to IS NULL" + ), + "temporal current-version index must be UNIQUE" + ); + assert!( + ddl.contains("CHECK (valid_to IS NULL OR valid_to >= valid_from)"), + "temporal valid_to ordering CHECK missing" + ); + } + /// Lineage edges must refuse self-loops at the storage layer /// (closes #42). The DAG claim in the README would be unenforced /// without this check. From 1c50285b6c3b62f15dfb8a921244552cc89213a7 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 14 May 2026 15:44:45 +0100 Subject: [PATCH 2/2] fix(codegen): provenance view uses ROW_NUMBER for latest-per-entity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #40. `generate_provenance_view` emitted a correlated MAX(timestamp) subquery whose outer reference `verisimdb_provenance_log.entity_id` was ambiguous: the same table appears in multiple nested scopes, and depending on planner choices the correlation either bound to the wrong scope (returning all rows for the entity) or worked accidentally. The view did not reliably return exactly one "latest" row per entity. Replace with a window-function pattern: ROW_NUMBER() OVER (PARTITION BY entity_id ORDER BY timestamp DESC) picked out as `_rn = 1`. No correlation, no scoping ambiguity, one row per entity by construction. Supported on PostgreSQL (always) and SQLite ≥ 3.25. Test `test_provenance_view_uses_window_function_for_latest_per_entity` asserts the new pattern is present and the old `MAX(p2.timestamp)` pattern is gone. `cargo clippy --all-targets -- -D warnings` clean; 38 unit tests pass. Co-Authored-By: Claude Opus 4.7 --- src/codegen/query.rs | 52 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/codegen/query.rs b/src/codegen/query.rs index dff08f4..2694a87 100644 --- a/src/codegen/query.rs +++ b/src/codegen/query.rs @@ -180,7 +180,13 @@ fn generate_provenance_view( table_name ); - // Use a subquery to get the latest provenance entry per entity. + // Select the latest provenance row per entity via ROW_NUMBER() over a + // PARTITION-by-entity / ORDER-by-timestamp-DESC window. The previous + // correlated MAX(timestamp) subquery referenced the outer + // `verisimdb_provenance_log` table by base-name, which is ambiguous + // when the same table appears in nested scopes and produced wrong + // (or no) "latest" rows depending on planner choices. Closes #40. + // ROW_NUMBER is supported on PostgreSQL (always) and SQLite ≥ 3.25. format!( "{comment}\ CREATE VIEW IF NOT EXISTS verisimdb_{table_name}_with_provenance AS\n\ @@ -193,14 +199,13 @@ fn generate_provenance_view( FROM {table_name}\n\ LEFT JOIN (\n\ \x20 SELECT entity_id, operation, actor, timestamp, hash\n\ - \x20 FROM verisimdb_provenance_log\n\ - \x20 WHERE table_name = '{table_name}'\n\ - \x20 AND timestamp = (\n\ - \x20 SELECT MAX(p2.timestamp)\n\ - \x20 FROM verisimdb_provenance_log p2\n\ - \x20 WHERE p2.entity_id = verisimdb_provenance_log.entity_id\n\ - \x20 AND p2.table_name = '{table_name}'\n\ - \x20 )\n\ + \x20 FROM (\n\ + \x20 SELECT entity_id, operation, actor, timestamp, hash,\n\ + \x20 ROW_NUMBER() OVER (PARTITION BY entity_id ORDER BY timestamp DESC) AS _rn\n\ + \x20 FROM verisimdb_provenance_log\n\ + \x20 WHERE table_name = '{table_name}'\n\ + \x20 ) ranked\n\ + \x20 WHERE _rn = 1\n\ ) prov ON prov.entity_id = ({entity_id_expr});\n\n", columns = column_list.join(",\n"), ) @@ -398,6 +403,35 @@ mod tests { assert!(interceptor.access_filter.is_none()); } + /// The provenance view must use a window-function `ROW_NUMBER()` pattern + /// instead of the old (broken) correlated MAX(timestamp) subquery. + /// Closes #40. + #[test] + fn test_provenance_view_uses_window_function_for_latest_per_entity() { + let schema = test_schema(); + let octad = OctadConfig { + enable_provenance: true, + enable_lineage: false, + enable_temporal: false, + enable_access_control: false, + enable_constraints: false, + enable_simulation: false, + }; + let interceptors = generate_interceptors(&schema, &octad, DatabaseBackend::SQLite); + let view = interceptors[0] + .provenance_view + .as_ref() + .expect("provenance view present when enabled"); + assert!( + view.contains("ROW_NUMBER() OVER (PARTITION BY entity_id ORDER BY timestamp DESC)"), + "provenance view should use window-function latest-per-entity" + ); + assert!( + !view.contains("MAX(p2.timestamp)"), + "old correlated MAX(timestamp) pattern must be gone" + ); + } + #[test] fn test_provenance_view_references_table() { let schema = test_schema();