Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions src/codegen/overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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\
Expand All @@ -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()
}

Expand Down Expand Up @@ -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.
Expand Down
52 changes: 43 additions & 9 deletions src/codegen/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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\
Expand All @@ -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"),
)
Expand Down Expand Up @@ -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();
Expand Down
Loading