Skip to content

BE-456: HashQL: Entity type model enrichment and graph store query extensions#8524

Open
indietyp wants to merge 5 commits intobm/be-429-hashql-per-island-entitypath-requirements-and-fetchislandfrom
bm/be-456-hashql-entity-type-model-enrichment-and-graph-store-query
Open

BE-456: HashQL: Entity type model enrichment and graph store query extensions#8524
indietyp wants to merge 5 commits intobm/be-429-hashql-per-island-entitypath-requirements-and-fetchislandfrom
bm/be-456-hashql-entity-type-model-enrichment-and-graph-store-query

Conversation

@indietyp
Copy link
Copy Markdown
Member

@indietyp indietyp commented Mar 8, 2026

🌟 What is the purpose of this PR?

Enriches the HashQL entity type model to match the full database schema, extends the graph-store SQL query builder with new expression types needed by the postgres compiler, and adds a database migration for the continuation composite type. This is the foundation layer for the postgres translation stack (BE-457, BE-306).

🔍 What does this change?

hashql-core:

  • Adds IdSnapshotVec, a typed-ID-indexed vector with transactional snapshot/rollback support (used by the postgres compiler for scoped state)
  • Expands the entity type definition with TemporalInterval, EntityTemporalMetadata, Confidence, provenance types, EntityMetadata, and LinkData fields to match the full database schema. Previously only EntityId, EntityRecordId, and partial LinkData were modeled.
  • Adds edition_id and ontology_type_ids symbols

graph-postgres-store query builder:

  • New expression types: FieldAccess (composite field extraction), JsonAgg, Constant::Null, Constant::U128, UnaryOperator::IsNotFalse
  • New cast targets: JsonB, Continuation, Numeric, Int, BigInt
  • Unnest now accepts multiple expressions (UNNEST(a, b, c))
  • SelectStatement gains OFFSET support
  • Convenience methods: .cast(), .not() and .grouped() now take self
  • Alias type changed from &'static str to Identifier for consistency
  • Visibility: mod postgres and mod table made pub for use by the HashQL postgres compiler
  • Unit tests for all new expression types

Database migration (v010__query):

  • Creates the continuation composite type: (filter boolean, block int, locals int[], values jsonb[]). This is the row type returned by compiled filter subqueries; the interpreter reads .filter to decide row inclusion and .block/.locals/.values to resume multi-block CFGs.

hashql-eval:

  • Reworks graph/read/path.rs to handle the expanded entity field structure
  • Scaffolding in lib.rs and error.rs for the postgres module (filled in by BE-306)

Test output updates:

  • ~55 files of updated .jsonc/.stdout/.stderr fixtures across eval (graph/read), HIR (lower/checking, specialization, graph-hoisting), and MIR (reify, inline) test suites, all cascading from the entity type changes

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

The entity type model still does not cover every field in the database schema (e.g., property_metadata inner structure is opaque). Fields are added as needed by the postgres compiler; remaining gaps will be filled incrementally.

🛡 What tests cover this?

  • IdSnapshotVec has doc tests and inline unit tests covering push/set/rollback/nested snapshots
  • New graph-store query builder tests for FieldAccess, JsonAgg, Null, U128, IsNotFalse, UNNEST with multiple args, cast types, and OFFSET
  • Existing compiletest suites (eval graph/read, HIR lowering, MIR reify/inline) with blessed output updates

❓ How to test this?

cargo nextest run -p hashql-core
cargo nextest run -p hashql-eval
cargo nextest run -p hash-graph-postgres-store --lib
cargo run --package hashql-compiletest -- run

indietyp added 3 commits March 8, 2026 16:17
feat: checkpoint (II)

feat: checkpoint (III)

feat: snapshot vec

feat: add dedicated filter

feat: checkpoint

feat: filter implementation

feat: filter implementation (mostly) done

chore: environment capture note

chore: always postgres bigint

feat: target clone

feat: simplify lookup

feat: move storage up

feat: eval entity path

chore: checkpoint

chore: checkpoint

chore: find entrypoint

feat: eval context

feat: eval cleanup

chore: cleanup

feat: track index

feat: wire up filter

feat: add error reporting

chore: checkpoint

feat: add traverse, and first postgres compiler outline

feat: traverse bitmap

feat: move traversal out

feat: projections

feat: projections

fix: clippy

feat: subquery projection for lateral

feat: checkpoint

feat: test plan

feat: checkpoint

feat: checkpoint – failing tests ;-;

feat: checkpoint – failing tests ;-;

feat: checkpoint — passing tests

fix: import

fix: entity type

feat: checkpoint

feat: attribute a cost to terminator placement switches

fix: import

feat: checkpoint

feat: checkpoint

chore: lint
@indietyp indietyp requested a review from a team as a code owner March 8, 2026 15:46
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview, Comment Mar 20, 2026 8:19am
petrinaut Ready Ready Preview Mar 20, 2026 8:19am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hashdotdesign Ignored Ignored Preview Mar 20, 2026 8:19am
hashdotdesign-tokens Ignored Ignored Preview Mar 20, 2026 8:19am

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 8, 2026

PR Summary

Medium Risk
Touches database migrations and core SQL query compilation primitives (new expression/transpile behavior, casts, and pagination), which can affect query correctness at runtime. Changes are mostly additive but broad, with many cascading fixture updates.

Overview
Introduces a new Postgres composite type continuation via migrations (v010__query and V49__query.sql) to support query/continuation handling.

Extends the graph Postgres SQL builder with new expression capabilities and typing: FieldAccess for composite field extraction, JsonAgg, UNNEST with multiple args, new constants (NULL, U128), new unary op IS NOT FALSE, additional cast targets (e.g. jsonb, continuation, numeric/int types), and SELECT ... OFFSET; also standardizes select aliases to use Identifier and exposes postgres/table modules publicly.

Enriches the HashQL Entity type surface to include metadata (temporal versioning, provenance, confidence, property metadata) and expanded LinkData, updates graph read path resolution and diagnostics accordingly, and blesses resulting eval/HIR fixture outputs.

Written by Cursor Bugbot for commit 61d002f. This will update automatically on new commits. Configure here.

@indietyp indietyp changed the title HashQL: Entity type model enrichment and graph store query extensions BE-456: HashQL: Entity type model enrichment and graph store query extensions Mar 8, 2026
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Misleading enum variant name for jsonb_agg function
    • Renamed Function::JsonAgg to Function::JsonbAgg and updated all internal usages/tests to match the emitted jsonb_agg(...) SQL.
  • ✅ Fixed: Multiple new symbols defined but never referenced
    • Removed the eight unreferenced symbol definitions from sym.rs to eliminate dead entries and duplicate provenance naming.

Create PR

Or push these changes by commenting:

@cursor push 928f4a2c0b
Preview (928f4a2c0b)
diff --git a/libs/@local/graph/postgres-store/src/store/postgres/query/expression/conditional.rs b/libs/@local/graph/postgres-store/src/store/postgres/query/expression/conditional.rs
--- a/libs/@local/graph/postgres-store/src/store/postgres/query/expression/conditional.rs
+++ b/libs/@local/graph/postgres-store/src/store/postgres/query/expression/conditional.rs
@@ -17,7 +17,7 @@
 pub enum Function {
     Min(Box<Expression>),
     Max(Box<Expression>),
-    JsonAgg(Box<Expression>),
+    JsonbAgg(Box<Expression>),
     JsonExtractText(Box<Expression>),
     JsonExtractAsText(Box<Expression>, PathToken<'static>),
     JsonExtractPath(Vec<Expression>),
@@ -55,7 +55,7 @@
                 expression.transpile(fmt)?;
                 fmt.write_char(')')
             }
-            Self::JsonAgg(expression) => {
+            Self::JsonbAgg(expression) => {
                 fmt.write_str("jsonb_agg(")?;
                 expression.transpile(fmt)?;
                 fmt.write_char(')')
@@ -821,9 +821,9 @@
     }
 
     #[test]
-    fn transpile_json_agg() {
+    fn transpile_jsonb_agg() {
         assert_eq!(
-            Expression::Function(Function::JsonAgg(Box::new(Expression::Parameter(1))))
+            Expression::Function(Function::JsonbAgg(Box::new(Expression::Parameter(1))))
                 .transpile_to_string(),
             "jsonb_agg($1)"
         );

diff --git a/libs/@local/hashql/core/src/symbol/sym.rs b/libs/@local/hashql/core/src/symbol/sym.rs
--- a/libs/@local/hashql/core/src/symbol/sym.rs
+++ b/libs/@local/hashql/core/src/symbol/sym.rs
@@ -36,7 +36,6 @@
     entity,
     entity_edition_id,
     entity_id,
-    entity_type_ids,
     entity_uuid,
     eq,
     Err,
@@ -55,8 +54,6 @@
     left_entity_confidence,
     left_entity_id,
     left_entity_provenance,
-    left_entity_uuid,
-    left_entity_web_id,
     link_data,
     List,
     lt,
@@ -76,10 +73,7 @@
     or,
     pow,
     properties,
-    property_metadata,
     provenance,
-    provenance_edition,
-    provenance_inferred,
     provided,
     r#as: "as",
     r#as_force: "as!",
@@ -101,8 +95,6 @@
     right_entity_confidence,
     right_entity_id,
     right_entity_provenance,
-    right_entity_uuid,
-    right_entity_web_id,
     Some,
     special_form,
     String,
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 8, 2026

Codecov Report

❌ Patch coverage is 66.17954% with 162 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.59%. Comparing base (b8fe72b) to head (61d002f).

Files with missing lines Patch % Lines
libs/@local/hashql/eval/src/graph/read/path.rs 13.54% 134 Missing ⚠️
libs/@local/hashql/core/src/id/snapshot_vec.rs 79.83% 25 Missing ⚠️
...store/src/store/postgres/query/statement/insert.rs 0.00% 2 Missing ⚠️
...src/store/postgres/query/expression/conditional.rs 98.85% 1 Missing ⚠️
Additional details and impacted files
@@                                           Coverage Diff                                           @@
##           bm/be-429-hashql-per-island-entitypath-requirements-and-fetchisland    #8524      +/-   ##
=======================================================================================================
- Coverage                                                                62.76%   61.59%   -1.18%     
=======================================================================================================
  Files                                                                     1326      915     -411     
  Lines                                                                   135528    98846   -36682     
  Branches                                                                  5523     4244    -1279     
=======================================================================================================
- Hits                                                                     85065    60880   -24185     
+ Misses                                                                   49549    37349   -12200     
+ Partials                                                                   914      617     -297     
Flag Coverage Δ
apps.hash-ai-worker-py ?
apps.hash-ai-worker-ts 1.40% <ø> (ø)
apps.hash-api 0.00% <ø> (ø)
backend-integration-tests ?
blockprotocol.type-system ?
deer ?
error-stack ?
local.claude-hooks ?
local.harpc-client ?
local.hash-backend-utils ?
local.hash-graph-sdk 9.63% <ø> (ø)
local.hash-isomorphic-utils 0.00% <ø> (ø)
local.hash-subgraph ?
rust.antsi ?
rust.deer ?
rust.error-stack ?
rust.harpc-codec ?
rust.harpc-net ?
rust.harpc-tower ?
rust.harpc-types ?
rust.harpc-wire-protocol ?
rust.hash-codec ?
rust.hash-graph-api 2.52% <ø> (ø)
rust.hash-graph-authorization ?
rust.hash-graph-postgres-store 26.81% <97.63%> (+0.42%) ⬆️
rust.hash-graph-store ?
rust.hash-graph-temporal-versioning ?
rust.hash-graph-types ?
rust.hash-graph-validation ?
rust.hashql-ast 87.23% <ø> (ø)
rust.hashql-compiletest 29.69% <ø> (ø)
rust.hashql-core 82.44% <87.30%> (+0.05%) ⬆️
rust.hashql-diagnostics ?
rust.hashql-eval 62.99% <13.54%> (-6.15%) ⬇️
rust.hashql-hir 89.06% <ø> (ø)
rust.hashql-mir 92.45% <ø> (ø)
rust.hashql-syntax-jexpr 94.05% <ø> (ø)
rust.sarif ?
sarif ?
tests.hash-backend-integration ?
unit-tests ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@augmentcode
Copy link
Copy Markdown

augmentcode bot commented Mar 8, 2026

🤖 Augment PR Summary

Summary: This PR enriches the HashQL entity type model to better reflect the full graph database schema, and extends the Postgres query-builder/AST to support additional SQL constructs needed by the HashQL→Postgres compiler.

Changes:

  • Adds a new DB migration creating a continuation composite type used as the return row type for compiled filter subqueries.
  • Introduces IdSnapshotVec, a typed-ID-indexed vector with snapshot/rollback support for scoped compiler state.
  • Expands ::graph::types::knowledge::entity with temporal metadata, confidence/provenance, enriched link data, entity metadata, and encodings.
  • Extends the graph Postgres query builder with composite field access, jsonb_agg, additional constants/casts, multi-arg UNNEST, and OFFSET.
  • Updates HashQL eval graph path resolution to traverse the new entity field structure.
  • Blesses updated UI/compiletest fixtures across eval/HIR/MIR to reflect the new entity model.

Technical Notes: Query builder aliasing is standardized on an Identifier type; new expression/unit tests cover the added SQL features.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 8, 2026

Merging this PR will degrade performance by 19.34%

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 78 untouched benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
pattern_match_constant 121.7 ns 150.8 ns -19.34%
type_checker_simulation 510.3 ns 462.5 ns +10.33%

Comparing bm/be-456-hashql-entity-type-model-enrichment-and-graph-store-query (61d002f) with bm/be-429-hashql-per-island-entitypath-requirements-and-fetchisland (74ce977)1

Open in CodSpeed

Footnotes

  1. No successful run was found on bm/be-429-hashql-per-island-entitypath-requirements-and-fetchisland (b8fe72b) during the generation of this report, so 59dc33f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@indietyp indietyp force-pushed the bm/be-456-hashql-entity-type-model-enrichment-and-graph-store-query branch from ef9b858 to a91293c Compare March 8, 2026 16:56
@indietyp indietyp force-pushed the bm/be-429-hashql-per-island-entitypath-requirements-and-fetchisland branch from 922e42b to 572197c Compare March 8, 2026 17:03
@indietyp indietyp force-pushed the bm/be-456-hashql-entity-type-model-enrichment-and-graph-store-query branch from a91293c to 43775ba Compare March 8, 2026 17:03
TimDiekmann
TimDiekmann previously approved these changes Mar 12, 2026
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 31, 2026

Deployment failed with the following error:

Invalid request: `attribution.gitUser` should NOT have additional property `isBot`.

@github-actions github-actions bot dismissed TimDiekmann’s stale review March 31, 2026 20:57

Latest approval commit 43775ba is not an ancestor of 61d002f, indicating rewritten history after approval


// Library Features
iterator_try_collect,
assert_matches,

Check warning

Code scanning / clippy

the feature assert_matches has been stable since 1.95.0 and no longer requires an attribute to enable Warning

the feature assert_matches has been stable since 1.95.0 and no longer requires an attribute to enable
// Library Features
iterator_try_collect,
assert_matches,
allocator_api,

Check warning

Code scanning / clippy

feature allocator_api is declared but not used Warning

feature allocator_api is declared but not used
iterator_try_collect,
assert_matches,
allocator_api,
iter_array_chunks,

Check warning

Code scanning / clippy

feature iter_array_chunks is declared but not used Warning

feature iter_array_chunks is declared but not used
assert_matches,
allocator_api,
iter_array_chunks,
maybe_uninit_fill

Check warning

Code scanning / clippy

feature maybe_uninit_fill is declared but not used Warning

feature maybe_uninit_fill is declared but not used
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Snapshot order not actually enforced
    • I added per-snapshot IDs with stack validation so commit/rollback now panic on out-of-order snapshot consumption.
  • ✅ Fixed: UNNEST now allows invalid empty call
    • I added an explicit non-empty assertion for Function::Unnest and a regression test to prevent emitting invalid UNNEST() SQL.
  • ✅ Fixed: Snapshot tokens not bound to vector
    • I bound snapshots to their originating IdSnapshotVec via an owner ID check so foreign snapshots are rejected.

Create PR

Or push these changes by commenting:

@cursor push 3658d0ed67
Preview (3658d0ed67)
diff --git a/libs/@local/graph/postgres-store/src/store/postgres/query/expression/conditional.rs b/libs/@local/graph/postgres-store/src/store/postgres/query/expression/conditional.rs
--- a/libs/@local/graph/postgres-store/src/store/postgres/query/expression/conditional.rs
+++ b/libs/@local/graph/postgres-store/src/store/postgres/query/expression/conditional.rs
@@ -123,6 +123,10 @@
                 fmt.write_char(')')
             }
             Self::Unnest(expression) => {
+                assert!(
+                    !expression.is_empty(),
+                    "UNNEST requires at least one argument"
+                );
                 fmt.write_str("UNNEST(")?;
 
                 for (index, element) in expression.iter().enumerate() {
@@ -841,6 +845,12 @@
     }
 
     #[test]
+    #[should_panic(expected = "UNNEST requires at least one argument")]
+    fn transpile_unnest_empty_panics() {
+        let _ = Expression::Function(Function::Unnest(vec![])).transpile_to_string();
+    }
+
+    #[test]
     fn transpile_field_access() {
         assert_eq!(
             Expression::FieldAccess {

diff --git a/libs/@local/hashql/core/src/id/snapshot_vec.rs b/libs/@local/hashql/core/src/id/snapshot_vec.rs
--- a/libs/@local/hashql/core/src/id/snapshot_vec.rs
+++ b/libs/@local/hashql/core/src/id/snapshot_vec.rs
@@ -37,10 +37,19 @@
     hash::{Hash, Hasher},
     mem,
     ops::Deref,
+    sync::atomic::{AtomicU64, Ordering},
 };
 
 use super::{Id, IdVec, slice::IdSlice};
 
+static NEXT_SNAPSHOT_OWNER_ID: AtomicU64 = AtomicU64::new(1);
+
+fn next_snapshot_owner_id() -> u64 {
+    let owner_id = NEXT_SNAPSHOT_OWNER_ID.fetch_add(1, Ordering::Relaxed);
+    assert!(owner_id != 0, "snapshot owner id exhausted");
+    owner_id
+}
+
 /// Defines how custom mutations are undone during rollback.
 ///
 /// Built-in implementations:
@@ -77,34 +86,56 @@
 
 struct Log<I, T, S: UndoStrategy<I, T>, A: Allocator = Global> {
     tape: Vec<LogEntry<I, T, S, A>, A>,
-    open: usize,
+    open_snapshots: Vec<u64, A>,
+    owner_id: u64,
+    next_snapshot_id: u64,
 }
 
 impl<I, T, S: UndoStrategy<I, T>> Log<I, T, S> {
     const fn new() -> Self {
         Self {
             tape: Vec::new(),
-            open: 0,
+            open_snapshots: Vec::new(),
+            owner_id: 0,
+            next_snapshot_id: 1,
         }
     }
 }
 
 impl<I, T, S: UndoStrategy<I, T>, A: Allocator> Log<I, T, S, A> {
-    const fn new_in(alloc: A) -> Self {
+    fn new_in(alloc: A) -> Self
+    where
+        A: Clone,
+    {
         Self {
-            tape: Vec::new_in(alloc),
-            open: 0,
+            tape: Vec::new_in(alloc.clone()),
+            open_snapshots: Vec::new_in(alloc),
+            owner_id: 0,
+            next_snapshot_id: 1,
         }
     }
 
     const fn recording(&self) -> bool {
-        self.open > 0
+        !self.open_snapshots.is_empty()
     }
 
-    const fn start(&mut self) -> Snapshot {
-        self.open += 1;
+    fn start(&mut self) -> Snapshot {
+        if self.owner_id == 0 {
+            self.owner_id = next_snapshot_owner_id();
+        }
+
+        let tape_len = self.tape.len();
+        let snapshot_id = self.next_snapshot_id;
+        self.next_snapshot_id = self
+            .next_snapshot_id
+            .checked_add(1)
+            .expect("snapshot id exhausted");
+        self.open_snapshots.push(snapshot_id);
+
         Snapshot {
-            tape_len: self.tape.len(),
+            tape_len,
+            owner_id: self.owner_id,
+            snapshot_id,
         }
     }
 
@@ -114,7 +145,9 @@
         I: Id,
     {
         self.assert(&snapshot);
-        self.open -= 1;
+        self.open_snapshots
+            .pop()
+            .expect("snapshot presence verified by assert");
 
         while self.tape.len() > snapshot.tape_len {
             let entry = self.tape.pop().expect("tape length verified by loop guard");
@@ -144,29 +177,49 @@
     #[expect(clippy::needless_pass_by_value)]
     fn commit(&mut self, snapshot: Snapshot) {
         self.assert(&snapshot);
-        self.open -= 1;
+        self.open_snapshots
+            .pop()
+            .expect("snapshot presence verified by assert");
 
-        if self.open == 0 {
+        if self.open_snapshots.is_empty() {
             self.tape.clear();
         }
     }
 
     fn assert(&self, snapshot: &Snapshot) {
         assert!(
-            self.tape.len() >= snapshot.tape_len,
+            !self.open_snapshots.is_empty(),
+            "no open snapshot to commit or rollback"
+        );
+        assert!(
+            snapshot.owner_id == self.owner_id,
+            "snapshot does not belong to this IdSnapshotVec"
+        );
+        assert!(
+            snapshot.tape_len
+                <= self.tape.len(),
             "snapshot tape_len exceeds current tape length"
         );
-        assert!(self.open > 0, "no open snapshot to commit or rollback");
+        assert!(
+            snapshot.snapshot_id
+                == *self
+                    .open_snapshots
+                    .last()
+                    .expect("snapshot presence verified by previous assert"),
+            "snapshot consumed out of stack order"
+        );
     }
 }
 
 /// Opaque snapshot token for [`IdSnapshotVec`].
 ///
-/// Must be consumed by [`IdSnapshotVec::rollback_to`] or [`IdSnapshotVec::commit`] in stack
-/// (LIFO) order. Dropping a `Snapshot` without consuming it will not undo changes, but
-/// subsequent snapshot operations may panic.
+/// Must be consumed by [`IdSnapshotVec::rollback_to`] or [`IdSnapshotVec::commit`] on the same
+/// vector instance and in stack (LIFO) order. Dropping a `Snapshot` without consuming it will not
+/// undo changes, but subsequent snapshot operations may panic.
 pub struct Snapshot {
     tape_len: usize,
+    owner_id: u64,
+    snapshot_id: u64,
 }
 
 /// A snapshot-capable vector that uses typed IDs for indexing.
@@ -497,7 +550,7 @@
     /// assert_eq!(vec.len(), 1);
     /// ```
     #[inline]
-    pub const fn snapshot(&mut self) -> Snapshot {
+    pub fn snapshot(&mut self) -> Snapshot {
         self.log.start()
     }
 
@@ -782,10 +835,36 @@
         let snap = vec.snapshot();
         vec.commit(snap);
 
-        vec.commit(Snapshot { tape_len: 0 });
+        vec.commit(Snapshot {
+            tape_len: 0,
+            owner_id: vec.log.owner_id,
+            snapshot_id: vec.log.next_snapshot_id,
+        });
     }
 
     #[test]
+    #[should_panic(expected = "snapshot consumed out of stack order")]
+    fn out_of_order_snapshot_consumption_panics() {
+        let mut vec = IdSnapshotVec::<TestId, i32>::new();
+        vec.push(1);
+        let outer = vec.snapshot();
+        let _inner = vec.snapshot();
+
+        vec.commit(outer);
+    }
+
+    #[test]
+    #[should_panic(expected = "snapshot does not belong to this IdSnapshotVec")]
+    fn foreign_snapshot_panics() {
+        let mut first = IdSnapshotVec::<TestId, i32>::new();
+        let mut second = IdSnapshotVec::<TestId, i32>::new();
+        let foreign = first.snapshot();
+        let _local = second.snapshot();
+
+        second.commit(foreign);
+    }
+
+    #[test]
     fn empty_snapshot_roundtrip() {
         let mut vec = IdSnapshotVec::<TestId, i32>::new();

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

"snapshot tape_len exceeds current tape length"
);
assert!(self.open > 0, "no open snapshot to commit or rollback");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snapshot order not actually enforced

High Severity

IdSnapshotVec::rollback_to and IdSnapshotVec::commit claim LIFO-only snapshots, but Log::assert only checks tape_len and open > 0. Out-of-order consumption can succeed, leaving open and tape inconsistent. This can keep recording() enabled without a valid snapshot and cause later incorrect rollbacks or delayed panics.

Additional Locations (2)
Fix in Cursor Fix in Web

/// subsequent snapshot operations may panic.
pub struct Snapshot {
tape_len: usize,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snapshot tokens not bound to vector

Medium Severity

Snapshot is an unscoped token (tape_len only), and rollback_to/commit accept it without checking origin. A snapshot from one IdSnapshotVec can be consumed by another if its state passes assertions, which can close or roll back the wrong transaction state and leave the original vector with an unconsumed snapshot.

Additional Locations (2)
Fix in Cursor Fix in Web


element.transpile(fmt)?;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNNEST now allows invalid empty call

Medium Severity

Function::Unnest now accepts Vec<Expression> and transpiles whatever length is provided. When the vector is empty, it emits UNNEST(), which is invalid SQL in PostgreSQL. The prior Box<Expression> API guaranteed at least one argument, so this introduces a new runtime query-failure path.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/libs Relates to first-party libraries/crates/packages (area) area/tests New or updated tests type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

3 participants