gqlite 1.5.1 → 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.
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
@@ -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
+ }