gqlite 1.5.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +10 -4
  3. data/ext/db-index/Cargo.toml +29 -0
  4. data/ext/db-index/src/hnsw/error.rs +66 -0
  5. data/ext/db-index/src/hnsw/graph_store.rs +272 -0
  6. data/ext/db-index/src/hnsw/id.rs +36 -0
  7. data/ext/db-index/src/hnsw/implementation.rs +1517 -0
  8. data/ext/db-index/src/hnsw/kernels.rs +1228 -0
  9. data/ext/db-index/src/hnsw/metric.rs +244 -0
  10. data/ext/db-index/src/hnsw/scalar.rs +72 -0
  11. data/ext/db-index/src/hnsw/simple_store.rs +140 -0
  12. data/ext/db-index/src/hnsw/store.rs +472 -0
  13. data/ext/db-index/src/hnsw/vector.rs +105 -0
  14. data/ext/db-index/src/hnsw/vectors.rs +568 -0
  15. data/ext/db-index/src/hnsw.rs +42 -0
  16. data/ext/db-index/src/lib.rs +3 -0
  17. data/ext/gqlitedb/Cargo.toml +19 -9
  18. data/ext/gqlitedb/benches/common/pokec.rs +62 -2
  19. data/ext/gqlitedb/benches/pokec_divan.rs +66 -2
  20. data/ext/gqlitedb/benches/pokec_iai.rs +60 -3
  21. data/ext/gqlitedb/release.toml +2 -2
  22. data/ext/gqlitedb/src/aggregators/arithmetic.rs +3 -1
  23. data/ext/gqlitedb/src/aggregators/containers.rs +1 -1
  24. data/ext/gqlitedb/src/aggregators/stats.rs +21 -12
  25. data/ext/gqlitedb/src/capi.rs +1 -1
  26. data/ext/gqlitedb/src/compiler/expression_analyser.rs +28 -20
  27. data/ext/gqlitedb/src/compiler/variables_manager.rs +97 -29
  28. data/ext/gqlitedb/src/compiler.rs +505 -225
  29. data/ext/gqlitedb/src/connection.rs +149 -11
  30. data/ext/gqlitedb/src/consts.rs +1 -2
  31. data/ext/gqlitedb/src/error.rs +123 -61
  32. data/ext/gqlitedb/src/functions/common.rs +39 -2
  33. data/ext/gqlitedb/src/functions/math.rs +8 -3
  34. data/ext/gqlitedb/src/functions/path.rs +48 -11
  35. data/ext/gqlitedb/src/functions/value.rs +1 -1
  36. data/ext/gqlitedb/src/functions.rs +23 -7
  37. data/ext/gqlitedb/src/graph.rs +1 -9
  38. data/ext/gqlitedb/src/interpreter/evaluators.rs +968 -130
  39. data/ext/gqlitedb/src/interpreter/instructions.rs +39 -2
  40. data/ext/gqlitedb/src/lib.rs +5 -4
  41. data/ext/gqlitedb/src/planner.rs +329 -0
  42. data/ext/gqlitedb/src/prelude.rs +3 -3
  43. data/ext/gqlitedb/src/store/pgrx.rs +1 -1
  44. data/ext/gqlitedb/src/store/postgres.rs +735 -7
  45. data/ext/gqlitedb/src/store/redb/hnsw_store.rs +702 -0
  46. data/ext/gqlitedb/src/store/redb/index.rs +274 -0
  47. data/ext/gqlitedb/src/store/redb.rs +1268 -113
  48. data/ext/gqlitedb/src/store/sqlbase/sqlmetadata.rs +28 -0
  49. data/ext/gqlitedb/src/store/sqlbase/sqlstore.rs +103 -0
  50. data/ext/gqlitedb/src/store/sqlbase.rs +146 -16
  51. data/ext/gqlitedb/src/store/sqlite.rs +569 -5
  52. data/ext/gqlitedb/src/store/vector_extract.rs +56 -0
  53. data/ext/gqlitedb/src/store.rs +123 -3
  54. data/ext/gqlitedb/src/tests/compiler.rs +207 -10
  55. data/ext/gqlitedb/src/tests/connection/postgres.rs +38 -3
  56. data/ext/gqlitedb/src/tests/connection/redb.rs +23 -0
  57. data/ext/gqlitedb/src/tests/connection/sqlite.rs +31 -0
  58. data/ext/gqlitedb/src/tests/connection.rs +61 -1
  59. data/ext/gqlitedb/src/tests/evaluators.rs +162 -7
  60. data/ext/gqlitedb/src/tests/parser.rs +54 -23
  61. data/ext/gqlitedb/src/tests/planner.rs +511 -0
  62. data/ext/gqlitedb/src/tests/store/postgres.rs +7 -0
  63. data/ext/gqlitedb/src/tests/store/redb.rs +8 -0
  64. data/ext/gqlitedb/src/tests/store/sqlite.rs +8 -0
  65. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +182 -0
  66. data/ext/gqlitedb/src/tests/store/vector_index/redb.rs +386 -0
  67. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +166 -0
  68. data/ext/gqlitedb/src/tests/store/vector_index.rs +2313 -0
  69. data/ext/gqlitedb/src/tests/store.rs +78 -14
  70. data/ext/gqlitedb/src/tests/templates/ast.rs +92 -16
  71. data/ext/gqlitedb/src/tests/templates/programs.rs +14 -7
  72. data/ext/gqlitedb/src/tests.rs +15 -9
  73. data/ext/gqlitedb/src/utils.rs +6 -1
  74. data/ext/gqlitedb/src/value/compare.rs +61 -3
  75. data/ext/gqlitedb/src/value.rs +136 -7
  76. data/ext/gqlitedb/templates/sql/postgres/metadata_delete.sql +1 -0
  77. data/ext/gqlitedb/templates/sql/postgres/node_select.sql +1 -1
  78. data/ext/gqlitedb/templates/sql/sqlite/metadata_delete.sql +1 -0
  79. data/ext/gqliterb/src/lib.rs +53 -8
  80. data/ext/gqlparser/Cargo.toml +25 -0
  81. data/ext/gqlparser/README.MD +9 -0
  82. data/ext/gqlparser/benches/pokec_divan.rs +34 -0
  83. data/ext/gqlparser/src/common.rs +69 -0
  84. data/ext/gqlparser/src/gqls/ast.rs +69 -0
  85. data/ext/gqlparser/src/gqls/constraint.rs +79 -0
  86. data/ext/gqlparser/src/gqls/error.rs +22 -0
  87. data/ext/gqlparser/src/gqls/parser.rs +813 -0
  88. data/ext/gqlparser/src/gqls/prelude.rs +8 -0
  89. data/ext/gqlparser/src/gqls/properties.rs +115 -0
  90. data/ext/gqlparser/src/gqls/resolve.rs +207 -0
  91. data/ext/gqlparser/src/gqls.rs +265 -0
  92. data/ext/gqlparser/src/lib.rs +7 -0
  93. data/ext/gqlparser/src/oc/ast.rs +680 -0
  94. data/ext/gqlparser/src/oc/error.rs +172 -0
  95. data/ext/gqlparser/src/oc/lexer.rs +429 -0
  96. data/ext/gqlparser/src/oc/parser/tests.rs +2284 -0
  97. data/ext/gqlparser/src/oc/parser.rs +2005 -0
  98. data/ext/gqlparser/src/oc.rs +33 -0
  99. data/ext/gqlparser/src/prelude.rs +3 -0
  100. data/ext/graphcore/Cargo.toml +1 -0
  101. data/ext/graphcore/src/error.rs +26 -0
  102. data/ext/graphcore/src/graph.rs +177 -51
  103. data/ext/graphcore/src/lib.rs +4 -2
  104. data/ext/graphcore/src/open_cypher.rs +12 -0
  105. data/ext/graphcore/src/table.rs +50 -2
  106. data/ext/graphcore/src/timestamp.rs +127 -104
  107. data/ext/graphcore/src/value/tensor.rs +739 -0
  108. data/ext/graphcore/src/value/value_map.rs +1 -1
  109. data/ext/graphcore/src/value.rs +343 -19
  110. metadata +90 -22
  111. data/ext/gqlitedb/src/parser/ast.rs +0 -604
  112. data/ext/gqlitedb/src/parser/parser_impl.rs +0 -1213
  113. data/ext/gqlitedb/src/parser.rs +0 -4
@@ -3,6 +3,7 @@ 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
9
  TransactionBoxable,
@@ -11,6 +12,20 @@ use crate::{
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;
@@ -86,7 +101,7 @@ impl Drop for TransactionBase
86
101
  if *self.active.borrow()
87
102
  && let Err(e) = self.connection.execute("ROLLBACK", ())
88
103
  {
89
- println!(
104
+ log::error!(
90
105
  "Rollback failed with error {:?}, future use of the connection are likely to fail.",
91
106
  e
92
107
  );
@@ -133,7 +148,7 @@ impl super::WriteTransaction for WriteTransaction
133
148
  }
134
149
  }
135
150
 
136
- trait GetConnection
151
+ pub(crate) trait GetConnection
137
152
  {
138
153
  fn get_connection(&self) -> &rusqlite::Connection;
139
154
  }
@@ -275,6 +290,16 @@ mod templates
275
290
  // ___) | || (_) | | | __/
276
291
  // |____/ \__\___/|_| \___|
277
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
+
278
303
  type TransactionBox = store::TransactionBox<ReadTransaction, WriteTransaction>;
279
304
 
280
305
  pub(crate) struct Store
@@ -290,7 +315,11 @@ impl Store
290
315
  {
291
316
  let path: PathBuf = path.as_ref().into();
292
317
  let connection = Pool::new(
293
- move || Ok(rusqlite::Connection::open(&path)?),
318
+ move || {
319
+ init_sqlite_vec();
320
+ let conn = rusqlite::Connection::open(&path)?;
321
+ Ok(conn)
322
+ },
294
323
  pool::Options::default().minimum_pool_size(1).pool_size(3),
295
324
  )?;
296
325
  let s = Self { connection };
@@ -302,10 +331,12 @@ impl Store
302
331
  let id = uuid::Uuid::new_v4().as_u128();
303
332
  let connection = Pool::new(
304
333
  move || {
305
- Ok(rusqlite::Connection::open_with_flags(
334
+ init_sqlite_vec();
335
+ let conn = rusqlite::Connection::open_with_flags(
306
336
  format!("file:{}?mode=memory&cache=shared", id),
307
337
  rusqlite::OpenFlags::default(),
308
- )?)
338
+ )?;
339
+ Ok(conn)
309
340
  },
310
341
  pool::Options::default().minimum_pool_size(1).pool_size(3),
311
342
  )?;
@@ -327,6 +358,69 @@ impl Store
327
358
  }
328
359
  match (from.major, from.minor)
329
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
+ }
330
424
  (1, 0) | (1, 1) =>
331
425
  {
332
426
  // uuid function is needed for upgrade
@@ -388,6 +482,10 @@ impl super::sqlbase::SqlMetaDataQueries for Store
388
482
  {
389
483
  Ok(include_str!("../../templates/sql/sqlite/metadata_set.sql").to_string())
390
484
  }
485
+ fn metadata_delete_query() -> Result<String>
486
+ {
487
+ Ok(include_str!("../../templates/sql/sqlite/metadata_delete.sql").to_string())
488
+ }
391
489
  }
392
490
 
393
491
  impl super::sqlbase::SqlStore for Store
@@ -516,6 +614,472 @@ impl super::sqlbase::SqlStore for Store
516
614
  |row| Ok(f(row)),
517
615
  )?
518
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
+ }
519
1083
  }
520
1084
 
521
1085
  impl super::sqlbase::SqlQueries for Store
@@ -0,0 +1,56 @@
1
+ use crate::value::Value;
2
+
3
+ /// Extract a flat Vec<f32> from a Value.
4
+ /// Handles Value::Tensor (new) and Value::Array (legacy).
5
+ pub(crate) fn extract_f32_vec(value: &Value) -> Option<Vec<f32>>
6
+ {
7
+ match value
8
+ {
9
+ Value::Tensor(t) => Some(t.to_f32_vec()),
10
+ Value::Array(arr) => arr
11
+ .iter()
12
+ .map(|v| match v
13
+ {
14
+ Value::Float(f) => Some(*f as f32),
15
+ Value::Integer(i) => Some(*i as f32),
16
+ _ => None,
17
+ })
18
+ .collect(),
19
+ _ => None,
20
+ }
21
+ }
22
+
23
+ #[cfg(any(feature = "sqlite", feature = "postgres", feature = "_pgrx"))]
24
+ fn extract_f32_from_json_value(val: &serde_json::Value) -> Option<Vec<f32>>
25
+ {
26
+ match val
27
+ {
28
+ serde_json::Value::Array(arr) => arr.iter().map(|e| e.as_f64().map(|f| f as f32)).collect(),
29
+ serde_json::Value::Object(map)
30
+ if map.get("__gql_type").and_then(|v| v.as_str()) == Some("tensor") =>
31
+ {
32
+ let arr = map.get("data")?.as_array()?;
33
+ arr.iter().map(|v| v.as_f64().map(|f| f as f32)).collect()
34
+ }
35
+ _ => None,
36
+ }
37
+ }
38
+
39
+ /// Extract a flat Vec<f32> from a raw JSON string.
40
+ /// Handles:
41
+ /// - legacy form: plain JSON array [1.0, 0.0, ...]
42
+ /// - new form: {"__gql_type":"tensor","dtype":"F32","shape":[...],"data":[...]}
43
+ #[cfg(any(feature = "sqlite", feature = "postgres", feature = "_pgrx"))]
44
+ pub(crate) fn extract_f32_vec_from_json(json_str: &str) -> Option<Vec<f32>>
45
+ {
46
+ let val: serde_json::Value = serde_json::from_str(json_str).ok()?;
47
+ extract_f32_from_json_value(&val)
48
+ }
49
+
50
+ /// Same as extract_f32_vec_from_json but for callers that already hold a serde_json::Value
51
+ /// (PostgreSQL retroactive population, where properties is a JSONB column).
52
+ #[cfg(any(feature = "postgres", feature = "_pgrx"))]
53
+ pub(crate) fn extract_f32_vec_from_serde_value(val: &serde_json::Value) -> Option<Vec<f32>>
54
+ {
55
+ extract_f32_from_json_value(val)
56
+ }