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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +12 -6
  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 -8
  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 +20 -24
  26. data/ext/gqlitedb/src/compiler/expression_analyser.rs +28 -20
  27. data/ext/gqlitedb/src/compiler/variables_manager.rs +104 -38
  28. data/ext/gqlitedb/src/compiler.rs +506 -227
  29. data/ext/gqlitedb/src/connection.rs +151 -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 +64 -0
  33. data/ext/gqlitedb/src/functions/containers.rs +2 -46
  34. data/ext/gqlitedb/src/functions/edge.rs +1 -1
  35. data/ext/gqlitedb/src/functions/math.rs +87 -15
  36. data/ext/gqlitedb/src/functions/node.rs +1 -1
  37. data/ext/gqlitedb/src/functions/path.rs +48 -11
  38. data/ext/gqlitedb/src/functions/scalar.rs +47 -4
  39. data/ext/gqlitedb/src/functions/string.rs +123 -1
  40. data/ext/gqlitedb/src/functions/value.rs +1 -1
  41. data/ext/gqlitedb/src/functions.rs +165 -28
  42. data/ext/gqlitedb/src/graph.rs +1 -9
  43. data/ext/gqlitedb/src/interpreter/evaluators.rs +968 -130
  44. data/ext/gqlitedb/src/interpreter/instructions.rs +39 -2
  45. data/ext/gqlitedb/src/lib.rs +5 -4
  46. data/ext/gqlitedb/src/parser/gql.pest +1 -1
  47. data/ext/gqlitedb/src/planner.rs +329 -0
  48. data/ext/gqlitedb/src/prelude.rs +3 -3
  49. data/ext/gqlitedb/src/store/pgrx.rs +1 -1
  50. data/ext/gqlitedb/src/store/postgres.rs +742 -16
  51. data/ext/gqlitedb/src/store/redb/hnsw_store.rs +702 -0
  52. data/ext/gqlitedb/src/store/redb/index.rs +274 -0
  53. data/ext/gqlitedb/src/store/redb.rs +1268 -113
  54. data/ext/gqlitedb/src/store/sqlbase/sqlmetadata.rs +28 -0
  55. data/ext/gqlitedb/src/store/sqlbase/sqlstore.rs +103 -0
  56. data/ext/gqlitedb/src/store/sqlbase.rs +146 -16
  57. data/ext/gqlitedb/src/store/sqlite.rs +576 -14
  58. data/ext/gqlitedb/src/store/vector_extract.rs +56 -0
  59. data/ext/gqlitedb/src/store.rs +140 -29
  60. data/ext/gqlitedb/src/tests/compiler.rs +207 -10
  61. data/ext/gqlitedb/src/tests/connection/postgres.rs +38 -3
  62. data/ext/gqlitedb/src/tests/connection/redb.rs +23 -0
  63. data/ext/gqlitedb/src/tests/connection/sqlite.rs +31 -0
  64. data/ext/gqlitedb/src/tests/connection.rs +61 -1
  65. data/ext/gqlitedb/src/tests/evaluators.rs +162 -7
  66. data/ext/gqlitedb/src/tests/parser.rs +54 -23
  67. data/ext/gqlitedb/src/tests/planner.rs +511 -0
  68. data/ext/gqlitedb/src/tests/store/postgres.rs +7 -0
  69. data/ext/gqlitedb/src/tests/store/redb.rs +8 -0
  70. data/ext/gqlitedb/src/tests/store/sqlite.rs +8 -0
  71. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +182 -0
  72. data/ext/gqlitedb/src/tests/store/vector_index/redb.rs +386 -0
  73. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +166 -0
  74. data/ext/gqlitedb/src/tests/store/vector_index.rs +2313 -0
  75. data/ext/gqlitedb/src/tests/store.rs +78 -14
  76. data/ext/gqlitedb/src/tests/templates/ast.rs +92 -16
  77. data/ext/gqlitedb/src/tests/templates/programs.rs +14 -7
  78. data/ext/gqlitedb/src/tests.rs +15 -9
  79. data/ext/gqlitedb/src/utils.rs +6 -1
  80. data/ext/gqlitedb/src/value/compare.rs +61 -3
  81. data/ext/gqlitedb/src/value.rs +136 -7
  82. data/ext/gqlitedb/templates/sql/postgres/metadata_delete.sql +1 -0
  83. data/ext/gqlitedb/templates/sql/postgres/node_select.sql +1 -1
  84. data/ext/gqlitedb/templates/sql/sqlite/metadata_delete.sql +1 -0
  85. data/ext/gqliterb/src/lib.rs +53 -8
  86. data/ext/gqlparser/Cargo.toml +25 -0
  87. data/ext/gqlparser/README.MD +9 -0
  88. data/ext/gqlparser/benches/pokec_divan.rs +34 -0
  89. data/ext/gqlparser/src/common.rs +69 -0
  90. data/ext/gqlparser/src/gqls/ast.rs +69 -0
  91. data/ext/gqlparser/src/gqls/constraint.rs +79 -0
  92. data/ext/gqlparser/src/gqls/error.rs +22 -0
  93. data/ext/gqlparser/src/gqls/parser.rs +813 -0
  94. data/ext/gqlparser/src/gqls/prelude.rs +8 -0
  95. data/ext/gqlparser/src/gqls/properties.rs +115 -0
  96. data/ext/gqlparser/src/gqls/resolve.rs +207 -0
  97. data/ext/gqlparser/src/gqls.rs +265 -0
  98. data/ext/gqlparser/src/lib.rs +7 -0
  99. data/ext/gqlparser/src/oc/ast.rs +680 -0
  100. data/ext/gqlparser/src/oc/error.rs +172 -0
  101. data/ext/gqlparser/src/oc/lexer.rs +429 -0
  102. data/ext/gqlparser/src/oc/parser/tests.rs +2284 -0
  103. data/ext/gqlparser/src/oc/parser.rs +2005 -0
  104. data/ext/gqlparser/src/oc.rs +33 -0
  105. data/ext/gqlparser/src/prelude.rs +3 -0
  106. data/ext/graphcore/Cargo.toml +1 -0
  107. data/ext/graphcore/src/error.rs +26 -0
  108. data/ext/graphcore/src/graph.rs +177 -51
  109. data/ext/graphcore/src/lib.rs +4 -2
  110. data/ext/graphcore/src/open_cypher.rs +12 -0
  111. data/ext/graphcore/src/table.rs +50 -2
  112. data/ext/graphcore/src/timestamp.rs +127 -104
  113. data/ext/graphcore/src/value/tensor.rs +739 -0
  114. data/ext/graphcore/src/value/value_map.rs +1 -1
  115. data/ext/graphcore/src/value.rs +343 -19
  116. metadata +93 -28
  117. data/ext/gqlitedb/src/parser/ast.rs +0 -604
  118. data/ext/gqlitedb/src/parser/parser_impl.rs +0 -1213
  119. data/ext/gqlitedb/src/parser.rs +0 -4
@@ -3,14 +3,29 @@ use std::{cell::RefCell, path::PathBuf};
3
3
  use ccutils::pool::{self, Pool};
4
4
 
5
5
  use crate::{
6
+ error::StoreError,
6
7
  prelude::*,
7
8
  store::{
8
- sqlbase::{SqlBindingValue, SqlResultValue},
9
9
  TransactionBoxable,
10
+ sqlbase::{SqlBindingValue, SqlResultValue},
10
11
  },
11
12
  utils::hex,
12
13
  };
13
14
 
15
+ // Initialize sqlite-vec extension
16
+ fn init_sqlite_vec()
17
+ {
18
+ static INIT: std::sync::Once = std::sync::Once::new();
19
+
20
+ INIT.call_once(|| unsafe {
21
+ use rusqlite::ffi::sqlite3_auto_extension;
22
+ use sqlite_vec::sqlite3_vec_init;
23
+
24
+ #[allow(clippy::missing_transmute_annotations)]
25
+ sqlite3_auto_extension(Some(std::mem::transmute(sqlite3_vec_init as *const ())));
26
+ });
27
+ }
28
+
14
29
  ccutils::assert_impl_all!(Store: Sync, Send);
15
30
 
16
31
  use askama::Template;
@@ -84,14 +99,12 @@ impl Drop for TransactionBase
84
99
  fn drop(&mut self)
85
100
  {
86
101
  if *self.active.borrow()
102
+ && let Err(e) = self.connection.execute("ROLLBACK", ())
87
103
  {
88
- if let Err(e) = self.connection.execute("ROLLBACK", ())
89
- {
90
- println!(
91
- "Rollback failed with error {:?}, future use of the connection are likely to fail.",
92
- e
93
- );
94
- }
104
+ log::error!(
105
+ "Rollback failed with error {:?}, future use of the connection are likely to fail.",
106
+ e
107
+ );
95
108
  }
96
109
  }
97
110
  }
@@ -135,7 +148,7 @@ impl super::WriteTransaction for WriteTransaction
135
148
  }
136
149
  }
137
150
 
138
- trait GetConnection
151
+ pub(crate) trait GetConnection
139
152
  {
140
153
  fn get_connection(&self) -> &rusqlite::Connection;
141
154
  }
@@ -277,6 +290,16 @@ mod templates
277
290
  // ___) | || (_) | | | __/
278
291
  // |____/ \__\___/|_| \___|
279
292
 
293
+ #[derive(Debug, serde::Serialize, serde::Deserialize)]
294
+ struct VectorIndexMetadata
295
+ {
296
+ name: String,
297
+ label: Option<String>,
298
+ property: Option<String>,
299
+ dimension: usize,
300
+ metric: store::VectorMetric,
301
+ }
302
+
280
303
  type TransactionBox = store::TransactionBox<ReadTransaction, WriteTransaction>;
281
304
 
282
305
  pub(crate) struct Store
@@ -292,7 +315,11 @@ impl Store
292
315
  {
293
316
  let path: PathBuf = path.as_ref().into();
294
317
  let connection = Pool::new(
295
- move || Ok(rusqlite::Connection::open(&path)?),
318
+ move || {
319
+ init_sqlite_vec();
320
+ let conn = rusqlite::Connection::open(&path)?;
321
+ Ok(conn)
322
+ },
296
323
  pool::Options::default().minimum_pool_size(1).pool_size(3),
297
324
  )?;
298
325
  let s = Self { connection };
@@ -304,10 +331,12 @@ impl Store
304
331
  let id = uuid::Uuid::new_v4().as_u128();
305
332
  let connection = Pool::new(
306
333
  move || {
307
- Ok(rusqlite::Connection::open_with_flags(
334
+ init_sqlite_vec();
335
+ let conn = rusqlite::Connection::open_with_flags(
308
336
  format!("file:{}?mode=memory&cache=shared", id),
309
337
  rusqlite::OpenFlags::default(),
310
- )?)
338
+ )?;
339
+ Ok(conn)
311
340
  },
312
341
  pool::Options::default().minimum_pool_size(1).pool_size(3),
313
342
  )?;
@@ -329,6 +358,69 @@ impl Store
329
358
  }
330
359
  match (from.major, from.minor)
331
360
  {
361
+ (1, 2) =>
362
+ {
363
+ // Migrate vector index metadata from the 1.2 custom-table format:
364
+ // _vector_indices_{graph} (SQL table with columns index_name, label, property, dimension, metric)
365
+ // to the 1.3 unified metadata format:
366
+ // {graph}/vector_indices -> Vec<VectorIndexMetadata>
367
+
368
+ let graphs: Vec<String> = self.get_metadata_value_json_or_default(transaction, "graphs")?;
369
+
370
+ for graph_name in &graphs
371
+ {
372
+ let metadata_table = format!("_vector_indices_{}", graph_name);
373
+ if !self.check_if_table_exists(transaction, &metadata_table)?
374
+ {
375
+ continue;
376
+ }
377
+
378
+ type LegacyIndexMetadata = (String, Option<String>, Option<String>, usize, String);
379
+ let rows: Vec<LegacyIndexMetadata> = {
380
+ let conn = transaction.get_connection();
381
+ let mut stmt = conn.prepare(&format!(
382
+ "SELECT index_name, label, property, dimension, metric FROM {}",
383
+ metadata_table
384
+ ))?;
385
+ stmt
386
+ .query_map([], |row| {
387
+ Ok((
388
+ row.get::<_, String>(0)?,
389
+ row.get::<_, Option<String>>(1)?,
390
+ row.get::<_, Option<String>>(2)?,
391
+ row.get::<_, i32>(3)? as usize,
392
+ row.get::<_, String>(4)?,
393
+ ))
394
+ })?
395
+ .collect::<Result<Vec<_>, _>>()?
396
+ };
397
+
398
+ let new_list: Vec<VectorIndexMetadata> = rows
399
+ .into_iter()
400
+ .map(
401
+ |(name, label, property, dimension, metric_str)| VectorIndexMetadata {
402
+ name,
403
+ label,
404
+ property,
405
+ dimension,
406
+ metric: store::VectorMetric::from_debug_str(&metric_str),
407
+ },
408
+ )
409
+ .collect();
410
+
411
+ self.set_metadata_value_json(
412
+ transaction,
413
+ format!("{}/vector_indices", graph_name),
414
+ &new_list,
415
+ )?;
416
+
417
+ transaction
418
+ .get_connection()
419
+ .execute(&format!("DROP TABLE IF EXISTS {}", metadata_table), ())?;
420
+ }
421
+
422
+ Ok(())
423
+ }
332
424
  (1, 0) | (1, 1) =>
333
425
  {
334
426
  // uuid function is needed for upgrade
@@ -390,6 +482,10 @@ impl super::sqlbase::SqlMetaDataQueries for Store
390
482
  {
391
483
  Ok(include_str!("../../templates/sql/sqlite/metadata_set.sql").to_string())
392
484
  }
485
+ fn metadata_delete_query() -> Result<String>
486
+ {
487
+ Ok(include_str!("../../templates/sql/sqlite/metadata_delete.sql").to_string())
488
+ }
393
489
  }
394
490
 
395
491
  impl super::sqlbase::SqlStore for Store
@@ -518,6 +614,472 @@ impl super::sqlbase::SqlStore for Store
518
614
  |row| Ok(f(row)),
519
615
  )?
520
616
  }
617
+
618
+ fn create_vector_index_impl(
619
+ &self,
620
+ transaction: &mut Self::TransactionBox,
621
+ graph_name: &str,
622
+ spec: &store::VectorIndexSpec,
623
+ ) -> Result<()>
624
+ {
625
+ if !spec.dtype.is_float()
626
+ {
627
+ return Err(
628
+ crate::error::StoreError::InvalidFormat(format!(
629
+ "SQLite vector index only supports floating-point dtypes (F32 or F64); got {:?}",
630
+ spec.dtype
631
+ ))
632
+ .into(),
633
+ );
634
+ }
635
+
636
+ let vec_table = format!("vec_{}_{}", graph_name, spec.name);
637
+
638
+ // Check if index already exists
639
+ let mut indices: Vec<VectorIndexMetadata> = self
640
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
641
+ if indices.iter().any(|m| m.name == spec.name)
642
+ {
643
+ return Err(
644
+ crate::error::StoreError::DuplicatedVectorIndex {
645
+ index_name: spec.name.clone(),
646
+ }
647
+ .into(),
648
+ );
649
+ }
650
+
651
+ transaction.get_connection().execute(
652
+ &format!(
653
+ "CREATE VIRTUAL TABLE IF NOT EXISTS {} USING vec0(embedding float[{}], node_id text)",
654
+ vec_table, spec.dimension
655
+ ),
656
+ (),
657
+ )?;
658
+
659
+ // Persist metadata for the new index
660
+ indices.push(VectorIndexMetadata {
661
+ name: spec.name.clone(),
662
+ label: spec.label.clone(),
663
+ property: spec.property.clone(),
664
+ dimension: spec.dimension,
665
+ metric: spec.metric,
666
+ });
667
+ self.set_metadata_value_json(
668
+ transaction,
669
+ format!("{}/vector_indices", graph_name),
670
+ &indices,
671
+ )?;
672
+
673
+ // Retroactively populate the index from existing nodes that match label+property
674
+ if let Some(label) = &spec.label
675
+ && let Some(property) = &spec.property
676
+ {
677
+ let nodes_table = format!("gqlite_{}_nodes", graph_name);
678
+ let sql = format!(
679
+ "SELECT hex(node_key), json_extract(properties, '$.{prop}') \
680
+ FROM {tbl} \
681
+ WHERE EXISTS ( \
682
+ SELECT 1 FROM json_each(labels) AS lbl WHERE lbl.value = ? \
683
+ ) \
684
+ AND json_extract(properties, '$.{prop}') IS NOT NULL",
685
+ prop = property,
686
+ tbl = nodes_table,
687
+ );
688
+ let rows: Vec<(String, String)> = {
689
+ let conn = transaction.get_connection();
690
+ match conn.prepare(&sql)
691
+ {
692
+ Ok(mut stmt) => stmt
693
+ .query_map(rusqlite::params![label], |row| {
694
+ Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
695
+ })?
696
+ .collect::<Result<Vec<_>, _>>()?,
697
+ Err(_) => Vec::new(),
698
+ }
699
+ };
700
+ for (node_key_hex, embedding_json) in rows
701
+ {
702
+ if let Some(vector) =
703
+ crate::store::vector_extract::extract_f32_vec_from_json(&embedding_json)
704
+ && !vector.is_empty()
705
+ && let Ok(uuid_val) = u128::from_str_radix(&node_key_hex, 16)
706
+ {
707
+ let key = graph::Key::new(uuid_val);
708
+ let tensor = value::Tensor::from_f32_slice(&vector);
709
+ let _ = self.upsert_vector_impl(transaction, graph_name, &spec.name, key, &tensor);
710
+ }
711
+ }
712
+ }
713
+
714
+ Ok(())
715
+ }
716
+
717
+ fn upsert_vector_impl(
718
+ &self,
719
+ transaction: &mut Self::TransactionBox,
720
+ graph_name: &str,
721
+ index_name: &str,
722
+ node_id: graph::Key,
723
+ tensor: &value::Tensor,
724
+ ) -> Result<()>
725
+ {
726
+ let f32_data: std::borrow::Cow<[f32]> = match tensor.dtype()
727
+ {
728
+ value::DType::F32 => match tensor.as_f32_slice()
729
+ {
730
+ Some(s) => std::borrow::Cow::Borrowed(s),
731
+ None => std::borrow::Cow::Owned(tensor.to_f32_vec()),
732
+ },
733
+ value::DType::F64 => std::borrow::Cow::Owned(tensor.to_f32_vec()),
734
+ other =>
735
+ {
736
+ return Err(
737
+ StoreError::InvalidFormat(format!(
738
+ "SQLite vector index only accepts F32 or F64 tensors; got {:?}",
739
+ other
740
+ ))
741
+ .into(),
742
+ );
743
+ }
744
+ };
745
+
746
+ let vec_table = format!("vec_{}_{}", graph_name, index_name);
747
+ let indices: Vec<VectorIndexMetadata> = self
748
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
749
+ let dimension = indices
750
+ .iter()
751
+ .find(|m| m.name == index_name)
752
+ .map(|m| m.dimension)
753
+ .ok_or_else(|| {
754
+ StoreError::InvalidFormat(format!(
755
+ "vector index '{}' does not exist in graph '{}'",
756
+ index_name, graph_name
757
+ ))
758
+ })?;
759
+
760
+ if f32_data.len() != dimension
761
+ {
762
+ return Err(
763
+ StoreError::InvalidFormat(format!(
764
+ "Vector dimension {} does not match index dimension {}",
765
+ f32_data.len(),
766
+ dimension
767
+ ))
768
+ .into(),
769
+ );
770
+ }
771
+
772
+ let vector_bytes: Vec<u8> = f32_data.iter().flat_map(|f| f.to_le_bytes()).collect();
773
+
774
+ let node_id_hex = hex(node_id);
775
+
776
+ transaction.get_connection().execute(
777
+ &format!("DELETE FROM {} WHERE node_id = ?", vec_table),
778
+ rusqlite::params![&node_id_hex],
779
+ )?;
780
+
781
+ transaction.get_connection().execute(
782
+ &format!(
783
+ "INSERT INTO {} (embedding, node_id) VALUES (?, ?)",
784
+ vec_table
785
+ ),
786
+ rusqlite::params![&vector_bytes[..], &node_id_hex],
787
+ )?;
788
+
789
+ Ok(())
790
+ }
791
+
792
+ fn list_vector_indexes_impl(
793
+ &self,
794
+ transaction: &mut Self::TransactionBox,
795
+ graph_name: &str,
796
+ ) -> Result<Vec<store::VectorIndexSpec>>
797
+ {
798
+ let indices: Vec<VectorIndexMetadata> = self
799
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
800
+
801
+ let specs = indices
802
+ .into_iter()
803
+ .map(|m| store::VectorIndexSpec {
804
+ name: m.name,
805
+ label: m.label,
806
+ property: m.property,
807
+ dimension: m.dimension,
808
+ metric: m.metric,
809
+ ..Default::default()
810
+ })
811
+ .collect();
812
+
813
+ Ok(specs)
814
+ }
815
+
816
+ fn vector_search_impl(
817
+ &self,
818
+ transaction: &mut Self::TransactionBox,
819
+ graph_name: &str,
820
+ index_name: &str,
821
+ query: &value::Tensor,
822
+ k: usize,
823
+ ) -> Result<Vec<store::VectorSearchResult>>
824
+ {
825
+ if k == 0
826
+ {
827
+ return Ok(Vec::new());
828
+ }
829
+
830
+ let f32_query: std::borrow::Cow<[f32]> = match query.dtype()
831
+ {
832
+ value::DType::F32 => match query.as_f32_slice()
833
+ {
834
+ Some(s) => std::borrow::Cow::Borrowed(s),
835
+ None => std::borrow::Cow::Owned(query.to_f32_vec()),
836
+ },
837
+ value::DType::F64 => std::borrow::Cow::Owned(query.to_f32_vec()),
838
+ other =>
839
+ {
840
+ return Err(
841
+ StoreError::InvalidFormat(format!(
842
+ "SQLite vector search only accepts F32 or F64 tensors; got {:?}",
843
+ other
844
+ ))
845
+ .into(),
846
+ );
847
+ }
848
+ };
849
+
850
+ let vec_table = format!("vec_{}_{}", graph_name, index_name);
851
+
852
+ let indices: Vec<VectorIndexMetadata> = self
853
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
854
+ let dimension = indices
855
+ .iter()
856
+ .find(|m| m.name == index_name)
857
+ .map(|m| m.dimension)
858
+ .ok_or_else(|| {
859
+ StoreError::InvalidFormat(format!(
860
+ "Vector index '{}' does not exist in graph '{}'",
861
+ index_name, graph_name
862
+ ))
863
+ })?;
864
+
865
+ if f32_query.len() != dimension
866
+ {
867
+ return Err(
868
+ StoreError::InvalidFormat(format!(
869
+ "Vector dimension {} does not match index dimension {}",
870
+ f32_query.len(),
871
+ dimension
872
+ ))
873
+ .into(),
874
+ );
875
+ }
876
+
877
+ let mut check_stmt = transaction
878
+ .get_connection()
879
+ .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")?;
880
+ let table_exists = check_stmt.exists(rusqlite::params![&vec_table])?;
881
+
882
+ if !table_exists
883
+ {
884
+ return Err(
885
+ StoreError::InvalidFormat(format!(
886
+ "Vector index '{}' does not exist in graph '{}'",
887
+ index_name, graph_name
888
+ ))
889
+ .into(),
890
+ );
891
+ }
892
+
893
+ let query_json = format!(
894
+ "[{}]",
895
+ f32_query
896
+ .iter()
897
+ .map(|f| f.to_string())
898
+ .collect::<Vec<_>>()
899
+ .join(", ")
900
+ );
901
+
902
+ let mut stmt = transaction.get_connection().prepare(&format!(
903
+ "SELECT node_id, distance FROM {}
904
+ WHERE embedding MATCH ?
905
+ ORDER BY distance
906
+ LIMIT ?",
907
+ vec_table
908
+ ))?;
909
+
910
+ let results = stmt
911
+ .query_map(rusqlite::params![&query_json, k as i64], |row| {
912
+ let node_id_hex: String = row.get(0)?;
913
+ let distance: f32 = row.get(1)?;
914
+ Ok((node_id_hex, distance))
915
+ })?
916
+ .collect::<Result<Vec<_>, _>>()?;
917
+
918
+ let mut search_results = Vec::new();
919
+ for (node_id_hex, distance) in results
920
+ {
921
+ if node_id_hex.len() != 32
922
+ {
923
+ return Err(
924
+ StoreError::InvalidFormat(format!(
925
+ "Invalid node_id encoding: expected 32 hex chars, got {}",
926
+ node_id_hex.len()
927
+ ))
928
+ .into(),
929
+ );
930
+ }
931
+
932
+ let key_uuid = u128::from_str_radix(&node_id_hex, 16).map_err(|_| {
933
+ StoreError::InvalidFormat(format!(
934
+ "Invalid hex encoding in node_id: '{}'",
935
+ node_id_hex
936
+ ))
937
+ })?;
938
+ let key = graph::Key::new(key_uuid);
939
+ search_results.push(store::VectorSearchResult {
940
+ node_id: key,
941
+ score: distance,
942
+ });
943
+ }
944
+
945
+ Ok(search_results)
946
+ }
947
+
948
+ fn drop_vector_index_impl(
949
+ &self,
950
+ transaction: &mut Self::TransactionBox,
951
+ graph_name: &str,
952
+ index_name: &str,
953
+ ) -> Result<()>
954
+ {
955
+ let mut indices: Vec<VectorIndexMetadata> = self
956
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
957
+
958
+ if !indices.iter().any(|m| m.name == index_name)
959
+ {
960
+ return Err(
961
+ StoreError::InvalidFormat(format!(
962
+ "Vector index '{}' does not exist in graph '{}'",
963
+ index_name, graph_name
964
+ ))
965
+ .into(),
966
+ );
967
+ }
968
+
969
+ let vec_table = format!("vec_{}_{}", graph_name, index_name);
970
+ transaction
971
+ .get_connection()
972
+ .execute(&format!("DROP TABLE IF EXISTS {}", vec_table), ())?;
973
+
974
+ indices.retain(|m| m.name != index_name);
975
+ self.set_metadata_value_json(
976
+ transaction,
977
+ format!("{}/vector_indices", graph_name),
978
+ &indices,
979
+ )?;
980
+
981
+ Ok(())
982
+ }
983
+
984
+ fn drop_graph_vector_indices_impl(
985
+ &self,
986
+ transaction: &mut Self::TransactionBox,
987
+ graph_name: &str,
988
+ ) -> Result<()>
989
+ {
990
+ let indices_key = format!("{}/vector_indices", graph_name);
991
+ let indices: Vec<VectorIndexMetadata> =
992
+ self.get_metadata_value_json_or_default(transaction, &indices_key)?;
993
+
994
+ for m in &indices
995
+ {
996
+ let vec_table = format!("vec_{}_{}", graph_name, m.name);
997
+ transaction
998
+ .get_connection()
999
+ .execute(&format!("DROP TABLE IF EXISTS {}", vec_table), ())?;
1000
+ }
1001
+
1002
+ self.delete_metadata(transaction, &indices_key)?;
1003
+ Ok(())
1004
+ }
1005
+
1006
+ fn get_vector_indices_for_property_impl(
1007
+ &self,
1008
+ transaction: &mut Self::TransactionBox,
1009
+ graph_name: &str,
1010
+ label: &str,
1011
+ property: &str,
1012
+ ) -> Result<Vec<(String, usize, store::VectorMetric)>>
1013
+ {
1014
+ let indices: Vec<VectorIndexMetadata> = self
1015
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
1016
+
1017
+ let result = indices
1018
+ .into_iter()
1019
+ .filter(|m| m.label.as_deref() == Some(label) && m.property.as_deref() == Some(property))
1020
+ .map(|m| (m.name, m.dimension, m.metric))
1021
+ .collect();
1022
+
1023
+ Ok(result)
1024
+ }
1025
+
1026
+ fn apply_vector_indices_for_node_impl(
1027
+ &self,
1028
+ transaction: &mut Self::TransactionBox,
1029
+ graph_name: &str,
1030
+ node: &crate::graph::Node,
1031
+ ) -> Result<()>
1032
+ {
1033
+ for label in node.labels()
1034
+ {
1035
+ for property in node.properties().keys()
1036
+ {
1037
+ let indices =
1038
+ self.get_vector_indices_for_property_impl(transaction, graph_name, label, property)?;
1039
+
1040
+ for (index_name, _dimension, _metric) in indices
1041
+ {
1042
+ if let Some(val) = node.properties().get(property)
1043
+ && let Some(vector) = crate::store::vector_extract::extract_f32_vec(val)
1044
+ && !vector.is_empty()
1045
+ {
1046
+ let tensor = value::Tensor::from_f32_slice(&vector);
1047
+ self
1048
+ .upsert_vector_impl(transaction, graph_name, &index_name, node.key(), &tensor)
1049
+ .ok();
1050
+ }
1051
+ }
1052
+ }
1053
+ }
1054
+
1055
+ Ok(())
1056
+ }
1057
+
1058
+ fn delete_vectors_for_nodes_impl(
1059
+ &self,
1060
+ transaction: &mut Self::TransactionBox,
1061
+ graph_name: &str,
1062
+ node_ids: &[String],
1063
+ ) -> Result<()>
1064
+ {
1065
+ let indices: Vec<VectorIndexMetadata> = self
1066
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))
1067
+ .unwrap_or_default();
1068
+
1069
+ for m in &indices
1070
+ {
1071
+ let vec_table = format!("vec_{}_{}", graph_name, m.name);
1072
+ for node_id in node_ids
1073
+ {
1074
+ let _ = transaction.get_connection().execute(
1075
+ &format!("DELETE FROM {} WHERE node_id = ?", vec_table),
1076
+ rusqlite::params![node_id],
1077
+ );
1078
+ }
1079
+ }
1080
+
1081
+ Ok(())
1082
+ }
521
1083
  }
522
1084
 
523
1085
  impl super::sqlbase::SqlQueries for Store
@@ -578,7 +1140,7 @@ impl super::sqlbase::SqlQueries for Store
578
1140
  }
579
1141
 
580
1142
  fn node_delete_query(graph_name: impl AsRef<str>, keys: impl AsRef<Vec<String>>)
581
- -> Result<String>
1143
+ -> Result<String>
582
1144
  {
583
1145
  Ok(
584
1146
  templates::NodeDelete {
@@ -627,7 +1189,7 @@ impl super::sqlbase::SqlQueries for Store
627
1189
  )
628
1190
  }
629
1191
  fn edge_delete_query(graph_name: impl AsRef<str>, keys: impl AsRef<Vec<String>>)
630
- -> Result<String>
1192
+ -> Result<String>
631
1193
  {
632
1194
  Ok(
633
1195
  templates::EdgeDelete {