gqlite 1.5.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +10 -4
  3. data/ext/db-index/Cargo.toml +29 -0
  4. data/ext/db-index/src/hnsw/error.rs +66 -0
  5. data/ext/db-index/src/hnsw/graph_store.rs +272 -0
  6. data/ext/db-index/src/hnsw/id.rs +36 -0
  7. data/ext/db-index/src/hnsw/implementation.rs +1517 -0
  8. data/ext/db-index/src/hnsw/kernels.rs +1228 -0
  9. data/ext/db-index/src/hnsw/metric.rs +244 -0
  10. data/ext/db-index/src/hnsw/scalar.rs +72 -0
  11. data/ext/db-index/src/hnsw/simple_store.rs +140 -0
  12. data/ext/db-index/src/hnsw/store.rs +472 -0
  13. data/ext/db-index/src/hnsw/vector.rs +105 -0
  14. data/ext/db-index/src/hnsw/vectors.rs +568 -0
  15. data/ext/db-index/src/hnsw.rs +42 -0
  16. data/ext/db-index/src/lib.rs +3 -0
  17. data/ext/gqlitedb/Cargo.toml +19 -9
  18. data/ext/gqlitedb/benches/common/pokec.rs +62 -2
  19. data/ext/gqlitedb/benches/pokec_divan.rs +66 -2
  20. data/ext/gqlitedb/benches/pokec_iai.rs +60 -3
  21. data/ext/gqlitedb/release.toml +2 -2
  22. data/ext/gqlitedb/src/aggregators/arithmetic.rs +3 -1
  23. data/ext/gqlitedb/src/aggregators/containers.rs +1 -1
  24. data/ext/gqlitedb/src/aggregators/stats.rs +21 -12
  25. data/ext/gqlitedb/src/capi.rs +1 -1
  26. data/ext/gqlitedb/src/compiler/expression_analyser.rs +28 -20
  27. data/ext/gqlitedb/src/compiler/variables_manager.rs +97 -29
  28. data/ext/gqlitedb/src/compiler.rs +505 -225
  29. data/ext/gqlitedb/src/connection.rs +149 -11
  30. data/ext/gqlitedb/src/consts.rs +1 -2
  31. data/ext/gqlitedb/src/error.rs +123 -61
  32. data/ext/gqlitedb/src/functions/common.rs +39 -2
  33. data/ext/gqlitedb/src/functions/math.rs +8 -3
  34. data/ext/gqlitedb/src/functions/path.rs +48 -11
  35. data/ext/gqlitedb/src/functions/value.rs +1 -1
  36. data/ext/gqlitedb/src/functions.rs +23 -7
  37. data/ext/gqlitedb/src/graph.rs +1 -9
  38. data/ext/gqlitedb/src/interpreter/evaluators.rs +968 -130
  39. data/ext/gqlitedb/src/interpreter/instructions.rs +39 -2
  40. data/ext/gqlitedb/src/lib.rs +5 -4
  41. data/ext/gqlitedb/src/planner.rs +329 -0
  42. data/ext/gqlitedb/src/prelude.rs +3 -3
  43. data/ext/gqlitedb/src/store/pgrx.rs +1 -1
  44. data/ext/gqlitedb/src/store/postgres.rs +735 -7
  45. data/ext/gqlitedb/src/store/redb/hnsw_store.rs +702 -0
  46. data/ext/gqlitedb/src/store/redb/index.rs +274 -0
  47. data/ext/gqlitedb/src/store/redb.rs +1268 -113
  48. data/ext/gqlitedb/src/store/sqlbase/sqlmetadata.rs +28 -0
  49. data/ext/gqlitedb/src/store/sqlbase/sqlstore.rs +103 -0
  50. data/ext/gqlitedb/src/store/sqlbase.rs +146 -16
  51. data/ext/gqlitedb/src/store/sqlite.rs +569 -5
  52. data/ext/gqlitedb/src/store/vector_extract.rs +56 -0
  53. data/ext/gqlitedb/src/store.rs +123 -3
  54. data/ext/gqlitedb/src/tests/compiler.rs +207 -10
  55. data/ext/gqlitedb/src/tests/connection/postgres.rs +38 -3
  56. data/ext/gqlitedb/src/tests/connection/redb.rs +23 -0
  57. data/ext/gqlitedb/src/tests/connection/sqlite.rs +31 -0
  58. data/ext/gqlitedb/src/tests/connection.rs +61 -1
  59. data/ext/gqlitedb/src/tests/evaluators.rs +162 -7
  60. data/ext/gqlitedb/src/tests/parser.rs +54 -23
  61. data/ext/gqlitedb/src/tests/planner.rs +511 -0
  62. data/ext/gqlitedb/src/tests/store/postgres.rs +7 -0
  63. data/ext/gqlitedb/src/tests/store/redb.rs +8 -0
  64. data/ext/gqlitedb/src/tests/store/sqlite.rs +8 -0
  65. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +182 -0
  66. data/ext/gqlitedb/src/tests/store/vector_index/redb.rs +386 -0
  67. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +166 -0
  68. data/ext/gqlitedb/src/tests/store/vector_index.rs +2313 -0
  69. data/ext/gqlitedb/src/tests/store.rs +78 -14
  70. data/ext/gqlitedb/src/tests/templates/ast.rs +92 -16
  71. data/ext/gqlitedb/src/tests/templates/programs.rs +14 -7
  72. data/ext/gqlitedb/src/tests.rs +15 -9
  73. data/ext/gqlitedb/src/utils.rs +6 -1
  74. data/ext/gqlitedb/src/value/compare.rs +61 -3
  75. data/ext/gqlitedb/src/value.rs +136 -7
  76. data/ext/gqlitedb/templates/sql/postgres/metadata_delete.sql +1 -0
  77. data/ext/gqlitedb/templates/sql/postgres/node_select.sql +1 -1
  78. data/ext/gqlitedb/templates/sql/sqlite/metadata_delete.sql +1 -0
  79. data/ext/gqliterb/src/lib.rs +53 -8
  80. data/ext/gqlparser/Cargo.toml +25 -0
  81. data/ext/gqlparser/README.MD +9 -0
  82. data/ext/gqlparser/benches/pokec_divan.rs +34 -0
  83. data/ext/gqlparser/src/common.rs +69 -0
  84. data/ext/gqlparser/src/gqls/ast.rs +69 -0
  85. data/ext/gqlparser/src/gqls/constraint.rs +79 -0
  86. data/ext/gqlparser/src/gqls/error.rs +22 -0
  87. data/ext/gqlparser/src/gqls/parser.rs +813 -0
  88. data/ext/gqlparser/src/gqls/prelude.rs +8 -0
  89. data/ext/gqlparser/src/gqls/properties.rs +115 -0
  90. data/ext/gqlparser/src/gqls/resolve.rs +207 -0
  91. data/ext/gqlparser/src/gqls.rs +265 -0
  92. data/ext/gqlparser/src/lib.rs +7 -0
  93. data/ext/gqlparser/src/oc/ast.rs +680 -0
  94. data/ext/gqlparser/src/oc/error.rs +172 -0
  95. data/ext/gqlparser/src/oc/lexer.rs +429 -0
  96. data/ext/gqlparser/src/oc/parser/tests.rs +2284 -0
  97. data/ext/gqlparser/src/oc/parser.rs +2005 -0
  98. data/ext/gqlparser/src/oc.rs +33 -0
  99. data/ext/gqlparser/src/prelude.rs +3 -0
  100. data/ext/graphcore/Cargo.toml +1 -0
  101. data/ext/graphcore/src/error.rs +26 -0
  102. data/ext/graphcore/src/graph.rs +177 -51
  103. data/ext/graphcore/src/lib.rs +4 -2
  104. data/ext/graphcore/src/open_cypher.rs +12 -0
  105. data/ext/graphcore/src/table.rs +50 -2
  106. data/ext/graphcore/src/timestamp.rs +127 -104
  107. data/ext/graphcore/src/value/tensor.rs +739 -0
  108. data/ext/graphcore/src/value/value_map.rs +1 -1
  109. data/ext/graphcore/src/value.rs +343 -19
  110. metadata +90 -22
  111. data/ext/gqlitedb/src/parser/ast.rs +0 -604
  112. data/ext/gqlitedb/src/parser/parser_impl.rs +0 -1213
  113. data/ext/gqlitedb/src/parser.rs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34702a81803ce382efb13f4206de1f1ba73329cd0a3ffe52238d32eb979a427a
4
- data.tar.gz: e9ccb531863f44072f0d6e58102296b32c54ad0f071772da755d031836d44b78
3
+ metadata.gz: 69d9620b19310b76b69a9bb2373cde64cfd94133e73b4197c51d59b0b5463c3d
4
+ data.tar.gz: 80af6abeddd183326704ae95cb5b00f2e41317e5a104d2a0c4d77c95cc4cdd5b
5
5
  SHA512:
6
- metadata.gz: 4cdf5b62762da1c470888c26d950e39e1843c91825f96b0a5cf4e6d39c4a26e981f16a6cef13153c21781996189569895310b758469d35777f0f71c56b7748ee
7
- data.tar.gz: '07697d27aea70038327f849f70231568824cd5a2b6a539eb4567476cf9def21f01469003050ec14ce5626e4b603dfdf5db79b9ab631267dc500672ab21c4f8ab'
6
+ metadata.gz: 1f14ae08b820f1e5e48960220912e01f74a41b83610571eec99e46647de2ebd48eed5925e280bf45f728973a7826b1da63cff2e526382bf5d0399b347043a247
7
+ data.tar.gz: 9de4b0738781537c2657c90bad7e1831064f8e783387973503971de741c2b861f651efc64733ecc406b0741dba2ba19cfb5f32fab83692b24f2be6a62b6057a2
data/ext/Cargo.toml CHANGED
@@ -3,20 +3,26 @@ resolver = "3"
3
3
  members = ["gqliterb", "gqlitedb", "graphcore"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.8.0"
6
+ version = "0.11.0"
7
7
  edition = "2024"
8
8
  license = "MIT"
9
9
  homepage = "https://gqlite.org"
10
10
  repository = "https://gitlab.com/gqlite/gqlite"
11
11
 
12
12
  [workspace.dependencies]
13
- graphcore = { version = "0.8.0", path = "graphcore" }
14
- gqlitedb = { version = "0.8.0", path = "gqlitedb" }
13
+ graphcore = { version = "0.11.0", path = "graphcore" }
14
+ gqlitedb = { version = "0.11.0", path = "gqlitedb" }
15
+ gqlparser = { version = "0.11.0", path = "gqlparser" }
16
+ db-index = { version = "0.11.0", path = "db-index" }
15
17
 
16
18
  askama = { version = "0.15" }
17
19
  ccutils = { version = "0.4" }
20
+ ciborium = "0.2"
21
+ divan = "0.1"
22
+ indexmap = "2"
18
23
  itertools = "0.14"
19
- rusqlite = { version = "0.37" }
24
+ log = "0.4"
25
+ rusqlite = { version = "0.38", features = ["load_extension"] }
20
26
  serde = "1"
21
27
  thiserror = "2"
22
28
  uuid = { version = "1", features = ["v4"] }
@@ -0,0 +1,29 @@
1
+ [package]
2
+ name = "db-index"
3
+ description = "Reusable indexing algorithms for key-value storage engines"
4
+ readme = "README.md"
5
+ license = "MIT"
6
+ homepage.workspace = true
7
+ repository.workspace = true
8
+ version = "0.11.0"
9
+ edition = "2021"
10
+
11
+ [dependencies]
12
+ ahash = "0.8"
13
+ arc-swap = "1"
14
+ bincode = "1"
15
+ bytemuck = "1"
16
+ ciborium.workspace = true
17
+ half = { version = "2", features = ["bytemuck"] }
18
+ ordered-float = "5"
19
+ parking_lot = "0.12"
20
+ serde = { version = "1", features = ["derive"] }
21
+ thiserror = "2"
22
+ tracing = "0.1"
23
+
24
+ [dev-dependencies]
25
+ approx = "0.5"
26
+ proptest = "1"
27
+ tempfile = "3"
28
+ rand_09 = { version = "0.9", package = "rand" }
29
+ divan = "0.1"
@@ -0,0 +1,66 @@
1
+ pub type Result<T, E = Error> = std::result::Result<T, E>;
2
+
3
+ const HNSW_META_SCHEMA_VERSION: u8 = 1;
4
+
5
+ #[derive(thiserror::Error, Debug)]
6
+ pub enum Error
7
+ {
8
+ #[error("dimension mismatch: expected {expected}, got {actual}")]
9
+ DimensionMismatch
10
+ {
11
+ expected: usize, actual: usize
12
+ },
13
+
14
+ #[error("key not found")]
15
+ KeyNotFound,
16
+
17
+ #[error("key already exists")]
18
+ KeyAlreadyExists,
19
+
20
+ #[error("index is empty")]
21
+ EmptyIndex,
22
+
23
+ #[error("missing vector")]
24
+ MissingVector,
25
+
26
+ #[error("invalid index format: {0}")]
27
+ InvalidIndexFormat(String),
28
+
29
+ #[error(transparent)]
30
+ Io(#[from] std::io::Error),
31
+
32
+ #[error("invalid qi8 scale: {scale}")]
33
+ InvalidQi8Scale
34
+ {
35
+ scale: f32
36
+ },
37
+
38
+ #[error("Schema version is too new: found {found} maximum is {HNSW_META_SCHEMA_VERSION}")]
39
+ SchemaVersionTooNew
40
+ {
41
+ found: u8
42
+ },
43
+ #[error("Schema version is too old: found {found} minim is {HNSW_META_SCHEMA_VERSION}")]
44
+ SchemaVersionUnsupported
45
+ {
46
+ found: u8
47
+ },
48
+ #[error("Malformed metadata: {0}")]
49
+ MalformedMetadata(String),
50
+ #[error("Empty decoded adjacency")]
51
+ EmptyDecodedAdjency,
52
+ #[error("Truncated decoded adjacency")]
53
+ TruncatedDecodedAdjency,
54
+ #[error("Trailing bytes after decoding adjacency")]
55
+ TrailingBytesDecodedAdjency,
56
+ #[error("Store error: {0}")]
57
+ StoreError(String),
58
+ }
59
+
60
+ impl From<std::convert::Infallible> for Error
61
+ {
62
+ fn from(_: std::convert::Infallible) -> Self
63
+ {
64
+ unreachable!("Infallible should never be instantiated")
65
+ }
66
+ }
@@ -0,0 +1,272 @@
1
+ use super::error::Error;
2
+ use crate::hnsw::id::NodeId;
3
+
4
+ /// Read-only graph store interface.
5
+ ///
6
+ /// The store tracks node allocation and connectivity. All absent nodes
7
+ /// (never-allocated or physically removed) are indistinguishable: methods
8
+ /// return `false` or `None` consistently. This is the contract that allows
9
+ /// stale links in surviving neighbors' adjacency lists to be safely skipped
10
+ /// during traversal without errors, closing the ghost-node bug class.
11
+ pub trait GraphStoreRead<K>
12
+ {
13
+ type Error: Into<Error>;
14
+
15
+ /// Allocation high-water mark (== next_node_id), NOT the live count.
16
+ /// This is what visited-set sizing needs; after delete churn the live
17
+ /// NodeId space is sparse within [0, node_count).
18
+ fn node_count(&self) -> Result<usize, Self::Error>;
19
+
20
+ /// Entry point: the node with the highest level (and that level).
21
+ /// None only for an empty graph.
22
+ fn entry_point(&self) -> Result<Option<(NodeId, i32)>, Self::Error>;
23
+
24
+ /// False for both never-allocated and physically-removed nodes alike.
25
+ /// The traversal guard is `if !graph.contains(n)? { continue; }` — stale
26
+ /// links to removed nodes are expected and must be skipped, not errors.
27
+ /// Absent must return clean `false`, never an error.
28
+ fn contains(&self, node: NodeId) -> Result<bool, Self::Error>;
29
+
30
+ /// None for absent nodes (never-allocated or removed), mirroring contains().
31
+ fn key_of(&self, node: NodeId) -> Result<Option<K>, Self::Error>;
32
+
33
+ /// None for absent nodes, mirroring contains().
34
+ fn node_of(&self, key: &K) -> Result<Option<NodeId>, Self::Error>;
35
+
36
+ /// Level of an existing node; Err for absent nodes.
37
+ /// Callers check contains() first on critical paths; this may error for
38
+ /// never-allocated nodes if the storage backend prefers that.
39
+ /// Precondition: `contains(node)` is true; implementations may panic otherwise.
40
+ fn level(&self, node: NodeId) -> Result<i32, Self::Error>;
41
+
42
+ /// Buffer-filling neighbors for a specific level.
43
+ /// Clears `out` before appending; absent nodes fill nothing (no error).
44
+ /// The redb impl cannot return references past an access guard, and the
45
+ /// hot loop copies candidates into its working set anyway.
46
+ fn neighbors(&self, node: NodeId, level: usize, out: &mut Vec<NodeId>)
47
+ -> Result<(), Self::Error>;
48
+ }
49
+
50
+ /// Write-capable graph store interface.
51
+ ///
52
+ /// Extends `GraphStoreRead<K>` with mutation operations. All writes are
53
+ /// immediate (no soft-delete or tombstone phase). NodeIds are monotonic and
54
+ /// never reused, even after removal, to prevent stale links from pointing at
55
+ /// wrong live nodes.
56
+ pub trait GraphStoreWrite<K>: GraphStoreRead<K>
57
+ {
58
+ /// Allocates the next NodeId (strictly increasing, never reused),
59
+ /// binds key<->id bidirectionally, and records the level in one atomic
60
+ /// operation. The adjacency list for all levels starts empty.
61
+ fn insert_node(&mut self, key: K, level: i32) -> Result<NodeId, Self::Error>;
62
+
63
+ /// Sets the complete neighbors list for a node at a specific level.
64
+ /// Overwrites any previous neighbors at that level.
65
+ fn set_neighbors(
66
+ &mut self,
67
+ node: NodeId,
68
+ level: usize,
69
+ neighbors: &[NodeId],
70
+ ) -> Result<(), Self::Error>;
71
+
72
+ /// Sets or clears the graph entry point.
73
+ fn set_entry_point(&mut self, ep: Option<(NodeId, i32)>) -> Result<(), Self::Error>;
74
+
75
+ /// Physically removes all rows for the node (adjacency, key, reverse
76
+ /// key, vector-side handled by the caller). The next_node_id is NOT
77
+ /// decremented, ever. After removal, `contains()` returns false and
78
+ /// `key_of()` / `node_of()` return None, but stale links in surviving
79
+ /// neighbors' adjacency lists remain untouched — traversal must skip them
80
+ /// via `contains()` checks.
81
+ fn remove_node(&mut self, node: NodeId) -> Result<(), Self::Error>;
82
+ }
83
+
84
+ /// # Conformance suite
85
+ ///
86
+ /// Every `GraphStoreWrite<K>` implementation must pass these tests. They are
87
+ /// public (not `#[cfg(test)]`) so that WI 367's gqlite cross-crate tests can
88
+ /// use them to validate the redb implementation.
89
+ /// Test key trait allowing conformance tests to work with any key type.
90
+ pub trait TestKey: Clone + PartialEq + std::fmt::Debug
91
+ {
92
+ fn sample_a() -> Self;
93
+ fn sample_b() -> Self;
94
+ fn sample_c() -> Self;
95
+ fn sample_d() -> Self;
96
+ }
97
+
98
+ impl TestKey for u128
99
+ {
100
+ fn sample_a() -> Self
101
+ {
102
+ 10
103
+ }
104
+ fn sample_b() -> Self
105
+ {
106
+ 20
107
+ }
108
+ fn sample_c() -> Self
109
+ {
110
+ 30
111
+ }
112
+ fn sample_d() -> Self
113
+ {
114
+ 40
115
+ }
116
+ }
117
+
118
+ pub fn conformance_alloc_is_monotonic_and_binds<K: TestKey, G: GraphStoreWrite<K>>(g: &mut G)
119
+ where
120
+ <G as GraphStoreRead<K>>::Error: std::fmt::Debug,
121
+ {
122
+ let a = g.insert_node(K::sample_a(), 0).unwrap();
123
+ let b = g.insert_node(K::sample_b(), 1).unwrap();
124
+ assert!(
125
+ b.as_u32() > a.as_u32(),
126
+ "allocation must be strictly increasing"
127
+ );
128
+ assert_eq!(
129
+ g.node_of(&K::sample_a()).unwrap(),
130
+ Some(a),
131
+ "key_of(node) must round-trip forward"
132
+ );
133
+ assert_eq!(
134
+ g.key_of(b).unwrap(),
135
+ Some(K::sample_b()),
136
+ "node_of(key) must round-trip backward"
137
+ );
138
+ assert_eq!(g.level(b).unwrap(), 1, "level must be stored and retrieved");
139
+ assert_eq!(
140
+ g.node_count().unwrap(),
141
+ 2,
142
+ "node_count must track high-water mark"
143
+ );
144
+ }
145
+
146
+ /// Tests that neighbors are stored and retrieved per-level, and that unset
147
+ /// levels read back empty without error.
148
+ pub fn conformance_neighbors_roundtrip_per_level<K: TestKey, G: GraphStoreWrite<K>>(g: &mut G)
149
+ where
150
+ <G as GraphStoreRead<K>>::Error: std::fmt::Debug,
151
+ {
152
+ let a = g.insert_node(K::sample_a(), 2).unwrap();
153
+ let b = g.insert_node(K::sample_b(), 0).unwrap();
154
+ let c = g.insert_node(K::sample_c(), 0).unwrap();
155
+ g.set_neighbors(a, 0, &[b, c]).unwrap();
156
+ g.set_neighbors(a, 2, &[c]).unwrap();
157
+
158
+ let mut out = Vec::new();
159
+ g.neighbors(a, 0, &mut out).unwrap();
160
+ assert_eq!(out, vec![b, c], "level 0 must return set neighbors");
161
+
162
+ g.neighbors(a, 1, &mut out).unwrap();
163
+ assert!(out.is_empty(), "unset level must return empty, not error");
164
+
165
+ g.neighbors(a, 2, &mut out).unwrap();
166
+ assert_eq!(out, vec![c], "level 2 must return its neighbors");
167
+ }
168
+
169
+ /// Tests that removal is immediate (no stale keys/nodes), IDs are never
170
+ /// reused even after removal, and stale links remain in surviving neighbors'
171
+ /// lists (must be skipped at traversal time).
172
+ pub fn conformance_remove_is_immediate_and_ids_not_reused<K: TestKey, G: GraphStoreWrite<K>>(
173
+ g: &mut G,
174
+ ) where
175
+ <G as GraphStoreRead<K>>::Error: std::fmt::Debug,
176
+ {
177
+ let a = g.insert_node(K::sample_a(), 0).unwrap();
178
+ let b = g.insert_node(K::sample_b(), 0).unwrap(); // highest id at this point
179
+ g.set_neighbors(a, 0, &[b]).unwrap();
180
+
181
+ g.remove_node(b).unwrap();
182
+ assert!(
183
+ !g.contains(b).unwrap(),
184
+ "removed node must return false from contains()"
185
+ );
186
+ assert_eq!(
187
+ g.key_of(b).unwrap(),
188
+ None,
189
+ "removed node must return None from key_of()"
190
+ );
191
+ assert_eq!(
192
+ g.node_of(&K::sample_b()).unwrap(),
193
+ None,
194
+ "removed key must return None from node_of()"
195
+ );
196
+
197
+ // Stale link survives in a's list; traversal-side skip is the contract.
198
+ let mut out = Vec::new();
199
+ g.neighbors(a, 0, &mut out).unwrap();
200
+ assert_eq!(
201
+ out,
202
+ vec![b],
203
+ "remove_node must NOT rewrite other nodes' adjacency lists"
204
+ );
205
+
206
+ // The invariant this design hangs on: removing the HIGHEST id must not free it for reuse.
207
+ let c = g.insert_node(K::sample_c(), 0).unwrap();
208
+ assert!(
209
+ c.as_u32() > b.as_u32(),
210
+ "removed id must never be reallocated (new id must be > removed id)"
211
+ );
212
+ assert_eq!(
213
+ g.node_count().unwrap(),
214
+ 3,
215
+ "node_count must be high-water mark, not live count"
216
+ );
217
+ }
218
+
219
+ /// Tests that entry point is stored and retrieved correctly, and can be
220
+ /// cleared.
221
+ pub fn conformance_entry_point_roundtrip<K: TestKey, G: GraphStoreWrite<K>>(g: &mut G)
222
+ where
223
+ <G as GraphStoreRead<K>>::Error: std::fmt::Debug,
224
+ {
225
+ assert_eq!(
226
+ g.entry_point().unwrap(),
227
+ None,
228
+ "empty graph has no entry point"
229
+ );
230
+
231
+ let a = g.insert_node(K::sample_a(), 3).unwrap();
232
+ g.set_entry_point(Some((a, 3))).unwrap();
233
+ assert_eq!(
234
+ g.entry_point().unwrap(),
235
+ Some((a, 3)),
236
+ "entry point must round-trip"
237
+ );
238
+
239
+ g.set_entry_point(None).unwrap();
240
+ assert_eq!(
241
+ g.entry_point().unwrap(),
242
+ None,
243
+ "entry point must be clearable"
244
+ );
245
+ }
246
+
247
+ /// Umbrella conformance suite: runs all conformance tests, creating a fresh
248
+ /// store for each test via the provided closure.
249
+ pub fn conformance_all<K: TestKey, G: GraphStoreWrite<K>>(mut fresh: impl FnMut() -> G)
250
+ where
251
+ <G as GraphStoreRead<K>>::Error: std::fmt::Debug,
252
+ {
253
+ {
254
+ let mut g = fresh();
255
+ conformance_alloc_is_monotonic_and_binds::<K, G>(&mut g);
256
+ } // g dropped here, explicitly, before the next fresh() call
257
+
258
+ {
259
+ let mut g = fresh();
260
+ conformance_neighbors_roundtrip_per_level::<K, G>(&mut g);
261
+ }
262
+
263
+ {
264
+ let mut g = fresh();
265
+ conformance_remove_is_immediate_and_ids_not_reused::<K, G>(&mut g);
266
+ }
267
+
268
+ {
269
+ let mut g = fresh();
270
+ conformance_entry_point_roundtrip::<K, G>(&mut g);
271
+ }
272
+ }
@@ -0,0 +1,36 @@
1
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
2
+ pub struct NodeId(u32);
3
+
4
+ impl NodeId
5
+ {
6
+ #[inline]
7
+ pub fn as_u32(self) -> u32
8
+ {
9
+ self.0
10
+ }
11
+
12
+ #[inline]
13
+ pub fn as_usize(self) -> usize
14
+ {
15
+ self.0 as usize
16
+ }
17
+
18
+ #[inline]
19
+ pub(crate) fn new(raw: u32) -> Self
20
+ {
21
+ Self(raw)
22
+ }
23
+
24
+ /// Reconstruct a NodeId from a raw value previously produced by
25
+ /// `as_u32` and persisted by a storage adapter. Adapters are the only
26
+ /// intended caller -- this intentionally does not enforce that `raw` was
27
+ /// ever actually allocated by this crate, since the only place that
28
+ /// matters (load-time validation) is `load_from_store` (WI 300), which
29
+ /// checks NodeId-set consistency across adjacency/key/vector data
30
+ /// regardless of where the NodeId values came from.
31
+ #[inline]
32
+ pub fn from_u32(raw: u32) -> Self
33
+ {
34
+ Self(raw)
35
+ }
36
+ }