gqlite 1.5.0 → 1.7.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.
- checksums.yaml +4 -4
- data/ext/Cargo.toml +12 -6
- data/ext/db-index/Cargo.toml +29 -0
- data/ext/db-index/src/hnsw/error.rs +66 -0
- data/ext/db-index/src/hnsw/graph_store.rs +272 -0
- data/ext/db-index/src/hnsw/id.rs +36 -0
- data/ext/db-index/src/hnsw/implementation.rs +1517 -0
- data/ext/db-index/src/hnsw/kernels.rs +1228 -0
- data/ext/db-index/src/hnsw/metric.rs +244 -0
- data/ext/db-index/src/hnsw/scalar.rs +72 -0
- data/ext/db-index/src/hnsw/simple_store.rs +140 -0
- data/ext/db-index/src/hnsw/store.rs +472 -0
- data/ext/db-index/src/hnsw/vector.rs +105 -0
- data/ext/db-index/src/hnsw/vectors.rs +568 -0
- data/ext/db-index/src/hnsw.rs +42 -0
- data/ext/db-index/src/lib.rs +3 -0
- data/ext/gqlitedb/Cargo.toml +19 -8
- data/ext/gqlitedb/benches/common/pokec.rs +62 -2
- data/ext/gqlitedb/benches/pokec_divan.rs +66 -2
- data/ext/gqlitedb/benches/pokec_iai.rs +60 -3
- data/ext/gqlitedb/release.toml +2 -2
- data/ext/gqlitedb/src/aggregators/arithmetic.rs +3 -1
- data/ext/gqlitedb/src/aggregators/containers.rs +1 -1
- data/ext/gqlitedb/src/aggregators/stats.rs +21 -12
- data/ext/gqlitedb/src/capi.rs +20 -24
- data/ext/gqlitedb/src/compiler/expression_analyser.rs +28 -20
- data/ext/gqlitedb/src/compiler/variables_manager.rs +104 -38
- data/ext/gqlitedb/src/compiler.rs +506 -227
- data/ext/gqlitedb/src/connection.rs +151 -11
- data/ext/gqlitedb/src/consts.rs +1 -2
- data/ext/gqlitedb/src/error.rs +123 -61
- data/ext/gqlitedb/src/functions/common.rs +64 -0
- data/ext/gqlitedb/src/functions/containers.rs +2 -46
- data/ext/gqlitedb/src/functions/edge.rs +1 -1
- data/ext/gqlitedb/src/functions/math.rs +87 -15
- data/ext/gqlitedb/src/functions/node.rs +1 -1
- data/ext/gqlitedb/src/functions/path.rs +48 -11
- data/ext/gqlitedb/src/functions/scalar.rs +47 -4
- data/ext/gqlitedb/src/functions/string.rs +123 -1
- data/ext/gqlitedb/src/functions/value.rs +1 -1
- data/ext/gqlitedb/src/functions.rs +165 -28
- data/ext/gqlitedb/src/graph.rs +1 -9
- data/ext/gqlitedb/src/interpreter/evaluators.rs +968 -130
- data/ext/gqlitedb/src/interpreter/instructions.rs +39 -2
- data/ext/gqlitedb/src/lib.rs +5 -4
- data/ext/gqlitedb/src/parser/gql.pest +1 -1
- data/ext/gqlitedb/src/planner.rs +329 -0
- data/ext/gqlitedb/src/prelude.rs +3 -3
- data/ext/gqlitedb/src/store/pgrx.rs +1 -1
- data/ext/gqlitedb/src/store/postgres.rs +742 -16
- data/ext/gqlitedb/src/store/redb/hnsw_store.rs +702 -0
- data/ext/gqlitedb/src/store/redb/index.rs +274 -0
- data/ext/gqlitedb/src/store/redb.rs +1268 -113
- data/ext/gqlitedb/src/store/sqlbase/sqlmetadata.rs +28 -0
- data/ext/gqlitedb/src/store/sqlbase/sqlstore.rs +103 -0
- data/ext/gqlitedb/src/store/sqlbase.rs +146 -16
- data/ext/gqlitedb/src/store/sqlite.rs +576 -14
- data/ext/gqlitedb/src/store/vector_extract.rs +56 -0
- data/ext/gqlitedb/src/store.rs +140 -29
- data/ext/gqlitedb/src/tests/compiler.rs +207 -10
- data/ext/gqlitedb/src/tests/connection/postgres.rs +38 -3
- data/ext/gqlitedb/src/tests/connection/redb.rs +23 -0
- data/ext/gqlitedb/src/tests/connection/sqlite.rs +31 -0
- data/ext/gqlitedb/src/tests/connection.rs +61 -1
- data/ext/gqlitedb/src/tests/evaluators.rs +162 -7
- data/ext/gqlitedb/src/tests/parser.rs +54 -23
- data/ext/gqlitedb/src/tests/planner.rs +511 -0
- data/ext/gqlitedb/src/tests/store/postgres.rs +7 -0
- data/ext/gqlitedb/src/tests/store/redb.rs +8 -0
- data/ext/gqlitedb/src/tests/store/sqlite.rs +8 -0
- data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +182 -0
- data/ext/gqlitedb/src/tests/store/vector_index/redb.rs +386 -0
- data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +166 -0
- data/ext/gqlitedb/src/tests/store/vector_index.rs +2313 -0
- data/ext/gqlitedb/src/tests/store.rs +78 -14
- data/ext/gqlitedb/src/tests/templates/ast.rs +92 -16
- data/ext/gqlitedb/src/tests/templates/programs.rs +14 -7
- data/ext/gqlitedb/src/tests.rs +15 -9
- data/ext/gqlitedb/src/utils.rs +6 -1
- data/ext/gqlitedb/src/value/compare.rs +61 -3
- data/ext/gqlitedb/src/value.rs +136 -7
- data/ext/gqlitedb/templates/sql/postgres/metadata_delete.sql +1 -0
- data/ext/gqlitedb/templates/sql/postgres/node_select.sql +1 -1
- data/ext/gqlitedb/templates/sql/sqlite/metadata_delete.sql +1 -0
- data/ext/gqliterb/src/lib.rs +53 -8
- data/ext/gqlparser/Cargo.toml +25 -0
- data/ext/gqlparser/README.MD +9 -0
- data/ext/gqlparser/benches/pokec_divan.rs +34 -0
- data/ext/gqlparser/src/common.rs +69 -0
- data/ext/gqlparser/src/gqls/ast.rs +69 -0
- data/ext/gqlparser/src/gqls/constraint.rs +79 -0
- data/ext/gqlparser/src/gqls/error.rs +22 -0
- data/ext/gqlparser/src/gqls/parser.rs +813 -0
- data/ext/gqlparser/src/gqls/prelude.rs +8 -0
- data/ext/gqlparser/src/gqls/properties.rs +115 -0
- data/ext/gqlparser/src/gqls/resolve.rs +207 -0
- data/ext/gqlparser/src/gqls.rs +265 -0
- data/ext/gqlparser/src/lib.rs +7 -0
- data/ext/gqlparser/src/oc/ast.rs +680 -0
- data/ext/gqlparser/src/oc/error.rs +172 -0
- data/ext/gqlparser/src/oc/lexer.rs +429 -0
- data/ext/gqlparser/src/oc/parser/tests.rs +2284 -0
- data/ext/gqlparser/src/oc/parser.rs +2005 -0
- data/ext/gqlparser/src/oc.rs +33 -0
- data/ext/gqlparser/src/prelude.rs +3 -0
- data/ext/graphcore/Cargo.toml +1 -0
- data/ext/graphcore/src/error.rs +26 -0
- data/ext/graphcore/src/graph.rs +177 -51
- data/ext/graphcore/src/lib.rs +4 -2
- data/ext/graphcore/src/open_cypher.rs +12 -0
- data/ext/graphcore/src/table.rs +50 -2
- data/ext/graphcore/src/timestamp.rs +127 -104
- data/ext/graphcore/src/value/tensor.rs +739 -0
- data/ext/graphcore/src/value/value_map.rs +1 -1
- data/ext/graphcore/src/value.rs +343 -19
- metadata +93 -28
- data/ext/gqlitedb/src/parser/ast.rs +0 -604
- data/ext/gqlitedb/src/parser/parser_impl.rs +0 -1213
- data/ext/gqlitedb/src/parser.rs +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0a8fea4c8e6f2e5eee4fd041a66f4cbe3ae35d8c3a0fc323dbea013efc14d30
|
|
4
|
+
data.tar.gz: 80af6abeddd183326704ae95cb5b00f2e41317e5a104d2a0c4d77c95cc4cdd5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e53d19758a9efaefcb1cac12d20d43045b1ea1a58578f2dbba8a5986797b77c04f133bbfbeb9309a522c0f44dc95fd8597201bcb7e78287792a7496c30462b0e
|
|
7
|
+
data.tar.gz: 9de4b0738781537c2657c90bad7e1831064f8e783387973503971de741c2b861f651efc64733ecc406b0741dba2ba19cfb5f32fab83692b24f2be6a62b6057a2
|
data/ext/Cargo.toml
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
[workspace]
|
|
2
|
-
resolver = "
|
|
2
|
+
resolver = "3"
|
|
3
3
|
members = ["gqliterb", "gqlitedb", "graphcore"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.
|
|
7
|
-
edition = "
|
|
6
|
+
version = "0.11.0"
|
|
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.
|
|
14
|
-
gqlitedb = { version = "0.
|
|
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
|
-
|
|
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
|
+
}
|