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
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
mod in_memory
|
|
2
|
+
{
|
|
3
|
+
crate::vector_index_test_suite!(
|
|
4
|
+
setup = |_| ((), crate::store::redb::Store::in_memory().unwrap())
|
|
5
|
+
);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
mod file_backed
|
|
9
|
+
{
|
|
10
|
+
crate::vector_index_test_suite!(
|
|
11
|
+
setup = |_| {
|
|
12
|
+
let tmp = crate::tests::create_tmp_file();
|
|
13
|
+
let path = tmp.path().to_path_buf();
|
|
14
|
+
let store = crate::store::redb::Store::open(path.as_path()).unwrap();
|
|
15
|
+
(tmp, store)
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
mod reopen
|
|
21
|
+
{
|
|
22
|
+
crate::vector_index_reopen_suite!(
|
|
23
|
+
setup = |_| {
|
|
24
|
+
let tmp = crate::tests::create_tmp_file();
|
|
25
|
+
let path = tmp.path().to_owned();
|
|
26
|
+
let factory = move || crate::store::redb::Store::open(&path).unwrap();
|
|
27
|
+
(tmp, factory)
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mod redb_specific
|
|
33
|
+
{
|
|
34
|
+
use crate::store::{Store, TransactionBoxable, VectorIndexSpec, VectorMetric};
|
|
35
|
+
use serial_test::serial;
|
|
36
|
+
|
|
37
|
+
#[test]
|
|
38
|
+
fn test_delete_persists_tombstone_without_reload()
|
|
39
|
+
{
|
|
40
|
+
let store = crate::store::redb::Store::in_memory().unwrap();
|
|
41
|
+
let graph_name = "delete_persist_no_reload";
|
|
42
|
+
let index_name = "idx";
|
|
43
|
+
|
|
44
|
+
let node_a = crate::graph::Node::new(
|
|
45
|
+
Some(graph_name.into()),
|
|
46
|
+
crate::graph::Key::new(1),
|
|
47
|
+
crate::graph::labels!("Item"),
|
|
48
|
+
crate::value::ValueMap::new(),
|
|
49
|
+
);
|
|
50
|
+
let node_b = crate::graph::Node::new(
|
|
51
|
+
Some(graph_name.into()),
|
|
52
|
+
crate::graph::Key::new(2),
|
|
53
|
+
crate::graph::labels!("Item"),
|
|
54
|
+
crate::value::ValueMap::new(),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
let mut tx = store.begin_write().unwrap();
|
|
58
|
+
store.create_graph(&mut tx, graph_name, false).unwrap();
|
|
59
|
+
store
|
|
60
|
+
.create_vector_index(
|
|
61
|
+
&mut tx,
|
|
62
|
+
graph_name,
|
|
63
|
+
VectorIndexSpec {
|
|
64
|
+
name: index_name.into(),
|
|
65
|
+
dimension: 3,
|
|
66
|
+
metric: VectorMetric::Cosine,
|
|
67
|
+
..Default::default()
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
.unwrap();
|
|
71
|
+
store
|
|
72
|
+
.create_nodes(&mut tx, graph_name, [&node_a, &node_b])
|
|
73
|
+
.unwrap();
|
|
74
|
+
store
|
|
75
|
+
.upsert_vector(
|
|
76
|
+
&mut tx,
|
|
77
|
+
graph_name,
|
|
78
|
+
index_name,
|
|
79
|
+
node_a.key(),
|
|
80
|
+
&crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
|
|
81
|
+
)
|
|
82
|
+
.unwrap();
|
|
83
|
+
store
|
|
84
|
+
.upsert_vector(
|
|
85
|
+
&mut tx,
|
|
86
|
+
graph_name,
|
|
87
|
+
index_name,
|
|
88
|
+
node_b.key(),
|
|
89
|
+
&crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
|
|
90
|
+
)
|
|
91
|
+
.unwrap();
|
|
92
|
+
tx.close().unwrap();
|
|
93
|
+
|
|
94
|
+
// Delete node_a and commit. No reload happens.
|
|
95
|
+
let mut tx = store.begin_write().unwrap();
|
|
96
|
+
store
|
|
97
|
+
.delete_nodes(
|
|
98
|
+
&mut tx,
|
|
99
|
+
graph_name,
|
|
100
|
+
crate::store::SelectNodeQuery::select_keys(vec![node_a.key()]),
|
|
101
|
+
true,
|
|
102
|
+
)
|
|
103
|
+
.unwrap();
|
|
104
|
+
tx.close().unwrap();
|
|
105
|
+
|
|
106
|
+
// After delete: node_a's rows are physically absent, node_b's remain.
|
|
107
|
+
let (adj_len, vec_len, key_len) = store
|
|
108
|
+
.test_hnsw_table_row_counts(graph_name, index_name)
|
|
109
|
+
.unwrap();
|
|
110
|
+
assert_eq!(
|
|
111
|
+
adj_len, 1,
|
|
112
|
+
"deleted node's adjacency rows must be physically removed, not tombstoned; \
|
|
113
|
+
only node_b's adjacency record remains"
|
|
114
|
+
);
|
|
115
|
+
assert_eq!(
|
|
116
|
+
vec_len, 1,
|
|
117
|
+
"deleted node's vector rows must be physically removed"
|
|
118
|
+
);
|
|
119
|
+
assert_eq!(
|
|
120
|
+
key_len, 1,
|
|
121
|
+
"deleted node's key records must be physically removed"
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Confirm search-level behavior excludes deleted node.
|
|
125
|
+
let mut tx = store.begin_read().unwrap();
|
|
126
|
+
let results = store
|
|
127
|
+
.vector_search(
|
|
128
|
+
&mut tx,
|
|
129
|
+
graph_name,
|
|
130
|
+
index_name,
|
|
131
|
+
&crate::value::Tensor::from_f32_slice(&[1.0, 0.0, 0.0]),
|
|
132
|
+
5,
|
|
133
|
+
)
|
|
134
|
+
.unwrap();
|
|
135
|
+
tx.close().unwrap();
|
|
136
|
+
assert!(
|
|
137
|
+
!results.iter().any(|r| r.node_id == node_a.key()),
|
|
138
|
+
"deleted node must not appear in search results"
|
|
139
|
+
);
|
|
140
|
+
assert!(
|
|
141
|
+
results.iter().any(|r| r.node_id == node_b.key()),
|
|
142
|
+
"surviving node must still be searchable"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[test]
|
|
147
|
+
#[serial]
|
|
148
|
+
fn test_deleted_vector_stays_deleted_after_reopen_without_reconciliation()
|
|
149
|
+
{
|
|
150
|
+
// This is WI #5.1's generic reopen test, re-asserted here in redb-specific
|
|
151
|
+
// form so it can also check that no reload-time reconciliation was needed:
|
|
152
|
+
// row counts before close and after reopen must be identical. If a
|
|
153
|
+
// reconciliation pass is still running, it may coincidentally produce the
|
|
154
|
+
// same *search* result while doing unnecessary work -- this test catches
|
|
155
|
+
// that by comparing table state, not just search output.
|
|
156
|
+
let temp_file = crate::tests::create_tmp_file();
|
|
157
|
+
let path = temp_file.path();
|
|
158
|
+
let graph_name = "deleted_stays_deleted_reopen";
|
|
159
|
+
let index_name = "idx";
|
|
160
|
+
|
|
161
|
+
let (adj_before, vec_before, key_before) = {
|
|
162
|
+
let store = crate::store::redb::Store::open(path).unwrap();
|
|
163
|
+
|
|
164
|
+
let nodes: Vec<_> = (1u128..=3)
|
|
165
|
+
.map(|id| {
|
|
166
|
+
crate::graph::Node::new(
|
|
167
|
+
Some(graph_name.into()),
|
|
168
|
+
crate::graph::Key::new(id),
|
|
169
|
+
crate::graph::labels!("Item"),
|
|
170
|
+
crate::value::ValueMap::new(),
|
|
171
|
+
)
|
|
172
|
+
})
|
|
173
|
+
.collect();
|
|
174
|
+
|
|
175
|
+
let mut tx = store.begin_write().unwrap();
|
|
176
|
+
store.create_graph(&mut tx, graph_name, false).unwrap();
|
|
177
|
+
store
|
|
178
|
+
.create_vector_index(
|
|
179
|
+
&mut tx,
|
|
180
|
+
graph_name,
|
|
181
|
+
VectorIndexSpec {
|
|
182
|
+
name: index_name.into(),
|
|
183
|
+
dimension: 3,
|
|
184
|
+
metric: VectorMetric::Cosine,
|
|
185
|
+
..Default::default()
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
.unwrap();
|
|
189
|
+
store
|
|
190
|
+
.create_nodes(&mut tx, graph_name, nodes.iter())
|
|
191
|
+
.unwrap();
|
|
192
|
+
for (node, v) in nodes
|
|
193
|
+
.iter()
|
|
194
|
+
.zip([[1.0f32, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
|
|
195
|
+
{
|
|
196
|
+
store
|
|
197
|
+
.upsert_vector(
|
|
198
|
+
&mut tx,
|
|
199
|
+
graph_name,
|
|
200
|
+
index_name,
|
|
201
|
+
node.key(),
|
|
202
|
+
&crate::value::Tensor::from_f32_slice(&v),
|
|
203
|
+
)
|
|
204
|
+
.unwrap();
|
|
205
|
+
}
|
|
206
|
+
tx.close().unwrap();
|
|
207
|
+
|
|
208
|
+
// Delete node 2 and commit.
|
|
209
|
+
let mut tx = store.begin_write().unwrap();
|
|
210
|
+
store
|
|
211
|
+
.delete_nodes(
|
|
212
|
+
&mut tx,
|
|
213
|
+
graph_name,
|
|
214
|
+
crate::store::SelectNodeQuery::select_keys(vec![nodes[1].key()]),
|
|
215
|
+
true,
|
|
216
|
+
)
|
|
217
|
+
.unwrap();
|
|
218
|
+
tx.close().unwrap();
|
|
219
|
+
|
|
220
|
+
store
|
|
221
|
+
.test_hnsw_table_row_counts(graph_name, index_name)
|
|
222
|
+
.unwrap()
|
|
223
|
+
}; // store dropped here
|
|
224
|
+
|
|
225
|
+
assert_eq!(
|
|
226
|
+
adj_before, 2,
|
|
227
|
+
"after delete: 2 live adjacency rows remain (deleted rows physically removed, not tombstoned)"
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
let store = crate::store::redb::Store::open(path).unwrap(); // reload runs here
|
|
231
|
+
|
|
232
|
+
let (adj_after, vec_after, key_after) = store
|
|
233
|
+
.test_hnsw_table_row_counts(graph_name, index_name)
|
|
234
|
+
.unwrap();
|
|
235
|
+
assert_eq!(
|
|
236
|
+
(adj_after, vec_after, key_after),
|
|
237
|
+
(adj_before, vec_before, key_before),
|
|
238
|
+
"reload must not add, remove, or otherwise touch persisted rows -- \
|
|
239
|
+
a reconciliation pass rewriting records would change these counts \
|
|
240
|
+
even if search output looked identical"
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
let mut tx = store.begin_read().unwrap();
|
|
244
|
+
let results = store
|
|
245
|
+
.vector_search(
|
|
246
|
+
&mut tx,
|
|
247
|
+
graph_name,
|
|
248
|
+
index_name,
|
|
249
|
+
&crate::value::Tensor::from_f32_slice(&[0.0, 1.0, 0.0]),
|
|
250
|
+
3,
|
|
251
|
+
)
|
|
252
|
+
.unwrap();
|
|
253
|
+
tx.close().unwrap();
|
|
254
|
+
|
|
255
|
+
assert!(
|
|
256
|
+
!results
|
|
257
|
+
.iter()
|
|
258
|
+
.any(|r| r.node_id == crate::graph::Key::new(2)),
|
|
259
|
+
"deleted node must not reappear after reopen"
|
|
260
|
+
);
|
|
261
|
+
assert_eq!(
|
|
262
|
+
results.len(),
|
|
263
|
+
2,
|
|
264
|
+
"exactly the 2 surviving nodes must be returned"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#[test]
|
|
269
|
+
#[serial]
|
|
270
|
+
fn test_repeated_delete_and_reopen_does_not_grow_persisted_table_size()
|
|
271
|
+
{
|
|
272
|
+
let temp_file = crate::tests::create_tmp_file();
|
|
273
|
+
let path = temp_file.path();
|
|
274
|
+
let graph_name = "no_leak_growth";
|
|
275
|
+
let index_name = "idx";
|
|
276
|
+
|
|
277
|
+
const N: usize = 20;
|
|
278
|
+
|
|
279
|
+
{
|
|
280
|
+
let store = crate::store::redb::Store::open(path).unwrap();
|
|
281
|
+
let nodes: Vec<_> = (1u128..=N as u128)
|
|
282
|
+
.map(|id| {
|
|
283
|
+
crate::graph::Node::new(
|
|
284
|
+
Some(graph_name.into()),
|
|
285
|
+
crate::graph::Key::new(id),
|
|
286
|
+
crate::graph::labels!("Item"),
|
|
287
|
+
crate::value::ValueMap::new(),
|
|
288
|
+
)
|
|
289
|
+
})
|
|
290
|
+
.collect();
|
|
291
|
+
|
|
292
|
+
let mut tx = store.begin_write().unwrap();
|
|
293
|
+
store.create_graph(&mut tx, graph_name, false).unwrap();
|
|
294
|
+
store
|
|
295
|
+
.create_vector_index(
|
|
296
|
+
&mut tx,
|
|
297
|
+
graph_name,
|
|
298
|
+
VectorIndexSpec {
|
|
299
|
+
name: index_name.into(),
|
|
300
|
+
dimension: 3,
|
|
301
|
+
metric: VectorMetric::Cosine,
|
|
302
|
+
..Default::default()
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
.unwrap();
|
|
306
|
+
store
|
|
307
|
+
.create_nodes(&mut tx, graph_name, nodes.iter())
|
|
308
|
+
.unwrap();
|
|
309
|
+
for (i, node) in nodes.iter().enumerate()
|
|
310
|
+
{
|
|
311
|
+
let v = [(i as f32) / N as f32, 0.0, 1.0 - (i as f32) / N as f32];
|
|
312
|
+
store
|
|
313
|
+
.upsert_vector(
|
|
314
|
+
&mut tx,
|
|
315
|
+
graph_name,
|
|
316
|
+
index_name,
|
|
317
|
+
node.key(),
|
|
318
|
+
&crate::value::Tensor::from_f32_slice(&v),
|
|
319
|
+
)
|
|
320
|
+
.unwrap();
|
|
321
|
+
}
|
|
322
|
+
tx.close().unwrap();
|
|
323
|
+
|
|
324
|
+
// Delete half.
|
|
325
|
+
let mut tx = store.begin_write().unwrap();
|
|
326
|
+
let to_delete: Vec<_> = nodes.iter().take(N / 2).map(|n| n.key()).collect();
|
|
327
|
+
store
|
|
328
|
+
.delete_nodes(
|
|
329
|
+
&mut tx,
|
|
330
|
+
graph_name,
|
|
331
|
+
crate::store::SelectNodeQuery::select_keys(to_delete),
|
|
332
|
+
true,
|
|
333
|
+
)
|
|
334
|
+
.unwrap();
|
|
335
|
+
tx.close().unwrap();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Reopen, then delete another quarter.
|
|
339
|
+
let (adj_after_first_round, _, _) = {
|
|
340
|
+
let store = crate::store::redb::Store::open(path).unwrap();
|
|
341
|
+
let remaining: Vec<u128> = ((N / 2 + 1) as u128..=N as u128).collect();
|
|
342
|
+
|
|
343
|
+
let mut tx = store.begin_write().unwrap();
|
|
344
|
+
let quarter = remaining.len() / 2;
|
|
345
|
+
let to_delete: Vec<_> = remaining
|
|
346
|
+
.iter()
|
|
347
|
+
.take(quarter)
|
|
348
|
+
.map(|&id| crate::graph::Key::new(id))
|
|
349
|
+
.collect();
|
|
350
|
+
store
|
|
351
|
+
.delete_nodes(
|
|
352
|
+
&mut tx,
|
|
353
|
+
graph_name,
|
|
354
|
+
crate::store::SelectNodeQuery::select_keys(to_delete),
|
|
355
|
+
true,
|
|
356
|
+
)
|
|
357
|
+
.unwrap();
|
|
358
|
+
tx.close().unwrap();
|
|
359
|
+
|
|
360
|
+
store
|
|
361
|
+
.test_hnsw_table_row_counts(graph_name, index_name)
|
|
362
|
+
.unwrap()
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Expected adjacency row count: N/2 rows after deleting N/2, then minus quarter
|
|
366
|
+
// more after the second delete. Final count: N/2 - (N/2)/2 = N/4 ≈ 5 rows.
|
|
367
|
+
// With physical deletion (no tombstones), deleted rows are immediately removed.
|
|
368
|
+
const EXPECTED_AFTER_SECOND_DELETE: usize = N / 2 / 2; // Quarter remains
|
|
369
|
+
assert_eq!(
|
|
370
|
+
adj_after_first_round, EXPECTED_AFTER_SECOND_DELETE,
|
|
371
|
+
"after deleting N/2 then reopen then delete quarter more, expect N/4 rows \
|
|
372
|
+
(physical deletion, no tombstones) -- growth beyond this would \
|
|
373
|
+
indicate reload duplicating records"
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// Reopen again; row count must be stable (no growth from the reopen itself).
|
|
377
|
+
let store = crate::store::redb::Store::open(path).unwrap();
|
|
378
|
+
let (adj_final, _, _) = store
|
|
379
|
+
.test_hnsw_table_row_counts(graph_name, index_name)
|
|
380
|
+
.unwrap();
|
|
381
|
+
assert_eq!(
|
|
382
|
+
adj_final, adj_after_first_round,
|
|
383
|
+
"a second reopen with no intervening writes must not change row count"
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
use crate::store::{
|
|
2
|
+
self, Store, TransactionBoxable as _, sqlbase::SqlStore, sqlite::GetConnection,
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
mod single_open
|
|
6
|
+
{
|
|
7
|
+
crate::vector_index_test_suite!(
|
|
8
|
+
setup = |_| {
|
|
9
|
+
let tmp = crate::tests::create_tmp_file();
|
|
10
|
+
let store = crate::store::sqlite::Store::open(tmp.path()).unwrap();
|
|
11
|
+
(tmp, store)
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
mod reopen
|
|
17
|
+
{
|
|
18
|
+
crate::vector_index_reopen_suite!(
|
|
19
|
+
setup = |_| {
|
|
20
|
+
let tmp = crate::tests::create_tmp_file();
|
|
21
|
+
let path = tmp.path().to_path_buf();
|
|
22
|
+
let factory = move || crate::store::sqlite::Store::open(path.as_path()).unwrap();
|
|
23
|
+
(tmp, factory)
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[test]
|
|
29
|
+
fn test_sqlite_vector_binary_roundtrip()
|
|
30
|
+
{
|
|
31
|
+
let original: Vec<f32> = vec![1.5, -2.25, 3.75];
|
|
32
|
+
|
|
33
|
+
let blob: Vec<u8> = original.iter().flat_map(|f| f.to_le_bytes()).collect();
|
|
34
|
+
|
|
35
|
+
let decoded: Vec<f32> = blob
|
|
36
|
+
.chunks_exact(4)
|
|
37
|
+
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
|
|
38
|
+
.collect();
|
|
39
|
+
|
|
40
|
+
assert_eq!(original, decoded);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn test_sqlite_vec_extension_available()
|
|
45
|
+
{
|
|
46
|
+
let tmp = crate::tests::create_tmp_file();
|
|
47
|
+
let store = crate::store::sqlite::Store::open(tmp.path()).unwrap();
|
|
48
|
+
|
|
49
|
+
let tx = store.begin_write().unwrap();
|
|
50
|
+
|
|
51
|
+
let res: Result<String, _> = tx
|
|
52
|
+
.get_connection()
|
|
53
|
+
.query_row("SELECT vec_version()", [], |row| row.get(0));
|
|
54
|
+
|
|
55
|
+
match res
|
|
56
|
+
{
|
|
57
|
+
Ok(v) => println!("sqlite-vec loaded: {}", v),
|
|
58
|
+
Err(e) => panic!("NOT loaded: {:?}", e),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let res = tx
|
|
62
|
+
.get_connection()
|
|
63
|
+
.execute("CREATE VIRTUAL TABLE test_vec USING vec0(c float[3])", ());
|
|
64
|
+
|
|
65
|
+
assert!(res.is_ok(), "sqlite-vec extension not loaded");
|
|
66
|
+
|
|
67
|
+
tx.close().unwrap();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn test_sqlite_vec_table_has_node_id_column()
|
|
72
|
+
{
|
|
73
|
+
let tmp = crate::tests::create_tmp_file();
|
|
74
|
+
let store = crate::store::sqlite::Store::open(tmp.path()).unwrap();
|
|
75
|
+
|
|
76
|
+
let mut tx = store.begin_write().unwrap();
|
|
77
|
+
|
|
78
|
+
store.create_graph(&mut tx, "g", false).unwrap();
|
|
79
|
+
store
|
|
80
|
+
.create_vector_index(
|
|
81
|
+
&mut tx,
|
|
82
|
+
"g",
|
|
83
|
+
store::VectorIndexSpec {
|
|
84
|
+
name: "idx".into(),
|
|
85
|
+
dimension: 3,
|
|
86
|
+
metric: store::VectorMetric::Cosine,
|
|
87
|
+
..Default::default()
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
.unwrap();
|
|
91
|
+
|
|
92
|
+
let conn = tx.get_connection();
|
|
93
|
+
|
|
94
|
+
let mut stmt = conn.prepare("PRAGMA table_info(vec_g_idx)").unwrap();
|
|
95
|
+
let cols: Vec<String> = stmt
|
|
96
|
+
.query_map([], |row| row.get::<_, String>(1))
|
|
97
|
+
.unwrap()
|
|
98
|
+
.map(|r| r.unwrap())
|
|
99
|
+
.collect();
|
|
100
|
+
drop(stmt);
|
|
101
|
+
|
|
102
|
+
assert!(
|
|
103
|
+
cols.contains(&"node_id".to_string()),
|
|
104
|
+
"vec table missing node_id column"
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
tx.close().unwrap();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn test_sqlite_metadata_round_trip()
|
|
112
|
+
{
|
|
113
|
+
let tmp = crate::tests::create_tmp_file();
|
|
114
|
+
let store = crate::store::sqlite::Store::open(tmp.path()).unwrap();
|
|
115
|
+
|
|
116
|
+
let mut tx = store.begin_write().unwrap();
|
|
117
|
+
for (name, metric) in [
|
|
118
|
+
("idx_cos", store::VectorMetric::Cosine),
|
|
119
|
+
("idx_l2", store::VectorMetric::L2),
|
|
120
|
+
("idx_dot", store::VectorMetric::DotProduct),
|
|
121
|
+
]
|
|
122
|
+
{
|
|
123
|
+
store
|
|
124
|
+
.create_vector_index(
|
|
125
|
+
&mut tx,
|
|
126
|
+
"default",
|
|
127
|
+
store::VectorIndexSpec {
|
|
128
|
+
name: name.to_string(),
|
|
129
|
+
label: Some("Item".to_string()),
|
|
130
|
+
property: Some("embedding".to_string()),
|
|
131
|
+
dimension: 3,
|
|
132
|
+
metric,
|
|
133
|
+
..Default::default()
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
.unwrap();
|
|
137
|
+
}
|
|
138
|
+
tx.close().unwrap();
|
|
139
|
+
|
|
140
|
+
let mut tx = store.begin_read().unwrap();
|
|
141
|
+
let mut indices =
|
|
142
|
+
<crate::store::sqlite::Store as SqlStore>::get_vector_indices_for_property_impl(
|
|
143
|
+
&store,
|
|
144
|
+
&mut tx,
|
|
145
|
+
"default",
|
|
146
|
+
"Item",
|
|
147
|
+
"embedding",
|
|
148
|
+
)
|
|
149
|
+
.unwrap();
|
|
150
|
+
tx.close().unwrap();
|
|
151
|
+
|
|
152
|
+
indices.sort_by(|a, b| a.0.cmp(&b.0));
|
|
153
|
+
assert_eq!(indices.len(), 3);
|
|
154
|
+
assert_eq!(
|
|
155
|
+
indices[0],
|
|
156
|
+
("idx_cos".to_string(), 3, store::VectorMetric::Cosine)
|
|
157
|
+
);
|
|
158
|
+
assert_eq!(
|
|
159
|
+
indices[1],
|
|
160
|
+
("idx_dot".to_string(), 3, store::VectorMetric::DotProduct)
|
|
161
|
+
);
|
|
162
|
+
assert_eq!(
|
|
163
|
+
indices[2],
|
|
164
|
+
("idx_l2".to_string(), 3, store::VectorMetric::L2)
|
|
165
|
+
);
|
|
166
|
+
}
|