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
@@ -1,22 +1,105 @@
1
1
  use std::cell::RefCell;
2
2
 
3
3
  use ccutils::pool::{self, Pool};
4
+ use serde::{Deserialize, Serialize};
4
5
 
5
6
  use crate::{
6
7
  prelude::*,
7
8
  store::{
9
+ TransactionBoxable, VectorMetric,
8
10
  sqlbase::{SqlBindingValue, SqlResultValue},
9
- TransactionBoxable,
10
11
  },
11
12
  };
12
13
 
13
14
  ccutils::assert_impl_all!(Store: Sync, Send);
14
15
 
15
16
  use askama::Template;
16
- use postgres::{types::ToSql, NoTls};
17
+ use postgres::{NoTls, types::ToSql};
17
18
 
18
19
  const SCHEMA: &str = "public";
19
20
 
21
+ /// Generate a vector index table name from graph and index names
22
+ fn vector_index_table_name(graph_name: &str, index_name: &str) -> String
23
+ {
24
+ format!("{}_{}", graph_name, index_name)
25
+ }
26
+
27
+ /// Convert hex-formatted u128 string back to UUID string format
28
+ fn format_hex_to_uuid(hex_str: &str) -> String
29
+ {
30
+ // Convert hex string back to u128, then to UUID
31
+ if let Ok(val) = u128::from_str_radix(hex_str, 16)
32
+ {
33
+ uuid::Uuid::from_u128(val).to_string()
34
+ }
35
+ else
36
+ {
37
+ // If parsing fails, return the original string (will cause SQL error)
38
+ hex_str.to_string()
39
+ }
40
+ }
41
+
42
+ fn cosine_distance(query: &[f32], stored: &[f32]) -> f32
43
+ {
44
+ if query.len() != stored.len()
45
+ {
46
+ return f32::INFINITY;
47
+ }
48
+ let query_norm = query.iter().map(|v| v * v).sum::<f32>().sqrt();
49
+ let stored_norm = stored.iter().map(|v| v * v).sum::<f32>().sqrt();
50
+ if query_norm == 0.0 || stored_norm == 0.0
51
+ {
52
+ return f32::INFINITY;
53
+ }
54
+ let dot = query
55
+ .iter()
56
+ .zip(stored.iter())
57
+ .map(|(a, b)| a * b)
58
+ .sum::<f32>();
59
+ let cosine_similarity = (dot / (query_norm * stored_norm)).clamp(-1.0, 1.0);
60
+ 1.0 - cosine_similarity
61
+ }
62
+
63
+ /// Map VectorMetric to pgvector operators
64
+ fn metric_to_pgvector_operator(metric: Option<&super::VectorMetric>) -> &'static str
65
+ {
66
+ match metric
67
+ {
68
+ Some(super::VectorMetric::Cosine) | None => "<=>",
69
+ Some(super::VectorMetric::L2) => "<->",
70
+ Some(super::VectorMetric::DotProduct) => "<#>",
71
+ }
72
+ }
73
+
74
+ /// Serialize a vector to pgvector binary format for use with postgres crate
75
+ /// pgvector expects vectors as arrays that can be passed as text
76
+ fn serialize_vector_for_pgvector(vector: &[f32]) -> String
77
+ {
78
+ format!(
79
+ "[{}]",
80
+ vector
81
+ .iter()
82
+ .map(|v| v.to_string())
83
+ .collect::<Vec<_>>()
84
+ .join(",")
85
+ )
86
+ }
87
+
88
+ /// Parse a vector from pgvector result
89
+ fn parse_pgvector_from_string(s: &str) -> Result<Vec<f32>>
90
+ {
91
+ s.trim_start_matches('[')
92
+ .trim_end_matches(']')
93
+ .split(',')
94
+ .map(|part| {
95
+ part
96
+ .trim()
97
+ .parse::<f32>()
98
+ .map_err(|e| StoreError::InvalidFormat(format!("Invalid vector component: {}", e)).into())
99
+ })
100
+ .collect()
101
+ }
102
+
20
103
  // _____ _____ ____ _
21
104
  // |_ _|__ | ___| __ ___ _ __ ___ / ___| __ _| |
22
105
  // | |/ _ \| |_ | '__/ _ \| '_ ` _ \\___ \ / _` | |
@@ -104,7 +187,28 @@ impl sqlbase::Row for postgres::Row
104
187
  .collect(),
105
188
  )),
106
189
  &Type::JSONB => Ok(SqlResultValue::JsonValue(self.try_get(index)?)),
107
- _ => Err(StoreError::UnsupportedPostgresType(ty.name().to_string()).into()),
190
+ _ =>
191
+ {
192
+ // Try to handle pgvector as a custom type - pgvector values come as text representation
193
+ if ty.name() == "vector"
194
+ {
195
+ let vector_str: String = self.try_get(index)?;
196
+ let vector = parse_pgvector_from_string(&vector_str)?;
197
+ // Return as blob since we don't have a dedicated vector type in SqlResultValue
198
+ // The caller will handle the blob as a vector
199
+ Ok(SqlResultValue::Blob(
200
+ vector
201
+ .iter()
202
+ .flat_map(|f| f.to_le_bytes())
203
+ .collect::<Vec<_>>()
204
+ .leak(),
205
+ ))
206
+ }
207
+ else
208
+ {
209
+ Err(StoreError::UnsupportedPostgresType(ty.name().to_string()).into())
210
+ }
211
+ }
108
212
  }
109
213
  }
110
214
  }
@@ -126,14 +230,12 @@ impl Drop for TransactionBase
126
230
  fn drop(&mut self)
127
231
  {
128
232
  if *self.active.borrow()
233
+ && let Err(e) = self.client.query("ROLLBACK", &[])
129
234
  {
130
- if let Err(e) = self.client.query("ROLLBACK", &[])
131
- {
132
- println!(
133
- "Rollback failed with error {:?}, future use of the connection are likely to fail.",
134
- e
135
- );
136
- }
235
+ log::error!(
236
+ "Rollback failed with error {:?}, future use of the connection are likely to fail.",
237
+ e
238
+ );
137
239
  }
138
240
  }
139
241
  }
@@ -307,6 +409,16 @@ mod templates
307
409
  }
308
410
  }
309
411
 
412
+ #[derive(Debug, Serialize, Deserialize)]
413
+ struct VectorIndexMetadata
414
+ {
415
+ name: String,
416
+ label: Option<String>,
417
+ property: Option<String>,
418
+ dimension: usize,
419
+ metric: VectorMetric,
420
+ }
421
+
310
422
  // ____ _
311
423
  // / ___|| |_ ___ _ __ ___
312
424
  // \___ \| __/ _ \| '__/ _ \
@@ -330,16 +442,103 @@ impl Store
330
442
  move || Ok(config.connect(NoTls)?),
331
443
  pool::Options::default().minimum_pool_size(1).pool_size(3),
332
444
  )?;
445
+ {
446
+ let mut setup_client = client.get()?;
447
+ setup_client.execute("CREATE EXTENSION IF NOT EXISTS vector", &[])?;
448
+ }
333
449
 
334
450
  let s = Self { client };
335
451
  s.initialise()?;
336
452
  Ok(s)
337
453
  }
338
- fn upgrade_database(&self, _transaction: &mut TransactionBox, _from: utils::Version)
339
- -> Result<()>
454
+ fn upgrade_database(&self, transaction: &mut TransactionBox, from: utils::Version) -> Result<()>
340
455
  {
341
- // No release with postgres, so nothing to upgrade
342
- Ok(())
456
+ match (from.major, from.minor)
457
+ {
458
+ (1, 2) =>
459
+ {
460
+ // Migrate vector index metadata from the 1.2 dual-key format:
461
+ // {graph}/vector_index_list -> Vec<String>
462
+ // {graph}/vector_indices/{name} -> VectorIndexMetadata (without name field)
463
+ // {graph}_{name}/metric -> VectorMetric (legacy, may or may not exist)
464
+ // to the 1.3 unified format:
465
+ // {graph}/vector_indices -> Vec<VectorIndexMetadata> (with name field)
466
+
467
+ #[derive(serde::Deserialize)]
468
+ struct LegacyVectorIndexMetadata
469
+ {
470
+ label: Option<String>,
471
+ property: Option<String>,
472
+ dimension: usize,
473
+ metric: VectorMetric,
474
+ }
475
+
476
+ let graphs: Vec<String> = self.get_metadata_value_json_or_default(transaction, "graphs")?;
477
+
478
+ for graph_name in &graphs
479
+ {
480
+ let list_key = format!("{}/vector_index_list", graph_name);
481
+ let index_names: Vec<String> =
482
+ self.get_metadata_value_json_or_default(transaction, &list_key)?;
483
+
484
+ let mut new_list: Vec<VectorIndexMetadata> = Vec::new();
485
+ for index_name in &index_names
486
+ {
487
+ let legacy_key = format!("{}/vector_indices/{}", graph_name, index_name);
488
+ match self
489
+ .get_metadata_value_json::<LegacyVectorIndexMetadata>(transaction, &legacy_key)
490
+ {
491
+ Ok(legacy) =>
492
+ {
493
+ new_list.push(VectorIndexMetadata {
494
+ name: index_name.clone(),
495
+ label: legacy.label,
496
+ property: legacy.property,
497
+ dimension: legacy.dimension,
498
+ metric: legacy.metric,
499
+ });
500
+ }
501
+ Err(e) =>
502
+ {
503
+ log::warn!(
504
+ "Skipping vector index '{}' in graph '{}' during migration: {}",
505
+ index_name,
506
+ graph_name,
507
+ e
508
+ );
509
+ }
510
+ }
511
+
512
+ // Clean up old per-index metadata key
513
+ self.delete_metadata(transaction, &legacy_key)?;
514
+
515
+ // Clean up legacy metric key if it exists
516
+ let table_name = vector_index_table_name(graph_name, index_name);
517
+ let metric_key = format!("{}/metric", table_name);
518
+ self.delete_metadata(transaction, &metric_key)?;
519
+ }
520
+
521
+ // Write consolidated list
522
+ self.set_metadata_value_json(
523
+ transaction,
524
+ format!("{}/vector_indices", graph_name),
525
+ &new_list,
526
+ )?;
527
+
528
+ // Remove old list key
529
+ self.delete_metadata(transaction, &list_key)?;
530
+ }
531
+
532
+ Ok(())
533
+ }
534
+ _ => Err(
535
+ StoreError::IncompatibleVersion {
536
+ expected: consts::GQLITE_VERSION,
537
+ actual: from,
538
+ }
539
+ .into(),
540
+ ),
541
+ }
343
542
  }
344
543
  /// Check if table exists
345
544
  pub(crate) fn check_if_table_exists(
@@ -368,6 +567,10 @@ impl super::sqlbase::SqlMetaDataQueries for Store
368
567
  {
369
568
  Ok(include_str!("../../templates/sql/postgres/metadata_set.sql").to_string())
370
569
  }
570
+ fn metadata_delete_query() -> Result<String>
571
+ {
572
+ Ok(include_str!("../../templates/sql/postgres/metadata_delete.sql").to_string())
573
+ }
371
574
  }
372
575
 
373
576
  impl super::sqlbase::SqlStore for Store
@@ -499,6 +702,529 @@ impl super::sqlbase::SqlStore for Store
499
702
  }
500
703
  Ok(())
501
704
  }
705
+
706
+ /// Create a vector index in PostgreSQL using pgvector extension
707
+ fn create_vector_index_impl(
708
+ &self,
709
+ transaction: &mut Self::TransactionBox,
710
+ graph_name: &str,
711
+ spec: &super::store::VectorIndexSpec,
712
+ ) -> Result<()>
713
+ {
714
+ if !spec.dtype.is_float()
715
+ {
716
+ return Err(
717
+ crate::error::StoreError::InvalidFormat(format!(
718
+ "PostgreSQL vector index only supports floating-point dtypes (F32 or F64); got {:?}",
719
+ spec.dtype
720
+ ))
721
+ .into(),
722
+ );
723
+ }
724
+
725
+ // Check if index already exists
726
+ let mut indices: Vec<VectorIndexMetadata> = self
727
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
728
+ if indices.iter().any(|m| m.name == spec.name)
729
+ {
730
+ return Err(
731
+ crate::error::StoreError::DuplicatedVectorIndex {
732
+ index_name: spec.name.clone(),
733
+ }
734
+ .into(),
735
+ );
736
+ }
737
+
738
+ let table_name = vector_index_table_name(graph_name, &spec.name);
739
+
740
+ // Create vector index table with node_id UUID and embedding vector columns
741
+ let create_table_sql = format!(
742
+ "CREATE TABLE IF NOT EXISTS {} (node_id UUID PRIMARY KEY, embedding vector({}))",
743
+ table_name, spec.dimension
744
+ );
745
+ transaction.get_client().execute(&create_table_sql, &[])?;
746
+
747
+ // Create index based on metric type
748
+ let (index_type, operator_class) = match spec.metric
749
+ {
750
+ super::store::VectorMetric::Cosine => ("hnsw", "vector_cosine_ops"),
751
+ super::store::VectorMetric::L2 => ("hnsw", "vector_l2_ops"),
752
+ super::store::VectorMetric::DotProduct => ("hnsw", "vector_ip_ops"),
753
+ };
754
+
755
+ let index_name = format!("idx_{}_{}", graph_name, spec.name);
756
+ let create_index_sql = format!(
757
+ "CREATE INDEX IF NOT EXISTS {} ON {} USING {} (embedding {})",
758
+ index_name, table_name, index_type, operator_class
759
+ );
760
+ transaction.get_client().execute(&create_index_sql, &[])?;
761
+
762
+ // Append metadata and persist the unified list
763
+ indices.push(VectorIndexMetadata {
764
+ name: spec.name.clone(),
765
+ label: spec.label.clone(),
766
+ property: spec.property.clone(),
767
+ dimension: spec.dimension,
768
+ metric: spec.metric,
769
+ });
770
+ self.set_metadata_value_json(
771
+ transaction,
772
+ format!("{}/vector_indices", graph_name),
773
+ &indices,
774
+ )?;
775
+
776
+ if let Some(label) = &spec.label
777
+ && let Some(property) = &spec.property
778
+ {
779
+ let nodes_table = format!("gqlite_{}_nodes", graph_name);
780
+ let select_nodes_sql = format!(
781
+ "SELECT node_key, properties FROM {} WHERE labels @> ARRAY[$1]",
782
+ nodes_table
783
+ );
784
+
785
+ let rows = match transaction.get_client().query(&select_nodes_sql, &[&label])
786
+ {
787
+ Ok(rows) => rows,
788
+ Err(_) => return Ok(()),
789
+ };
790
+
791
+ for row in rows
792
+ {
793
+ let node_uuid: uuid::Uuid = row.get(0);
794
+
795
+ // Get properties as either String or directly as serde_json::Value
796
+ let props = match row.try_get::<_, String>(1)
797
+ {
798
+ Ok(json_str) => serde_json::from_str::<serde_json::Value>(&json_str).ok(),
799
+ Err(_) =>
800
+ {
801
+ // Try getting as JSON directly
802
+ row.try_get::<_, serde_json::Value>(1).ok()
803
+ }
804
+ };
805
+
806
+ if let Some(props) = props
807
+ && let Some(value) = props.get(property)
808
+ && let Some(vector) =
809
+ crate::store::vector_extract::extract_f32_vec_from_serde_value(value)
810
+ && !vector.is_empty()
811
+ && !vector.iter().any(|v| !v.is_finite())
812
+ {
813
+ let vector_str = serialize_vector_for_pgvector(&vector);
814
+ let insert_sql = format!(
815
+ "INSERT INTO {} (node_id, embedding) VALUES ($1, '{}'::vector) ON CONFLICT (node_id) DO UPDATE SET embedding = EXCLUDED.embedding",
816
+ table_name, vector_str
817
+ );
818
+ transaction
819
+ .get_client()
820
+ .execute(&insert_sql, &[&node_uuid])?;
821
+ }
822
+ }
823
+ }
824
+
825
+ Ok(())
826
+ }
827
+
828
+ /// Upsert a vector embedding in PostgreSQL
829
+ fn upsert_vector_impl(
830
+ &self,
831
+ transaction: &mut Self::TransactionBox,
832
+ graph_name: &str,
833
+ index_name: &str,
834
+ node_id: crate::graph::Key,
835
+ tensor: &crate::value::Tensor,
836
+ ) -> Result<()>
837
+ {
838
+ let f32_data: std::borrow::Cow<[f32]> = match tensor.dtype()
839
+ {
840
+ crate::value::DType::F32 => match tensor.as_f32_slice()
841
+ {
842
+ Some(s) => std::borrow::Cow::Borrowed(s),
843
+ None => std::borrow::Cow::Owned(tensor.to_f32_vec()),
844
+ },
845
+ crate::value::DType::F64 => std::borrow::Cow::Owned(tensor.to_f32_vec()),
846
+ other =>
847
+ {
848
+ return Err(
849
+ StoreError::InvalidFormat(format!(
850
+ "PostgreSQL vector index only accepts F32 or F64 tensors; got {:?}",
851
+ other
852
+ ))
853
+ .into(),
854
+ );
855
+ }
856
+ };
857
+
858
+ if f32_data.iter().any(|v| !v.is_finite())
859
+ {
860
+ return Err(
861
+ StoreError::InvalidFormat("vector contains non-finite value (NaN or Inf)".to_string())
862
+ .into(),
863
+ );
864
+ }
865
+
866
+ let table_name = vector_index_table_name(graph_name, index_name);
867
+ let client = transaction.get_client();
868
+
869
+ // Serialize vector to pgvector format
870
+ let vector_str = serialize_vector_for_pgvector(&f32_data);
871
+
872
+ // Convert node_id to UUID
873
+ let node_uuid = uuid::Uuid::from_u128(node_id.uuid());
874
+
875
+ // Use PostgreSQL UPSERT pattern - embed vector in SQL since postgres-rs doesn't support pgvector parameters
876
+ let upsert_sql = format!(
877
+ "INSERT INTO {} (node_id, embedding) VALUES ($1, '{}'::vector) \
878
+ ON CONFLICT (node_id) DO UPDATE SET embedding = EXCLUDED.embedding",
879
+ table_name, vector_str
880
+ );
881
+ let result = client.execute(&upsert_sql, &[&node_uuid]);
882
+ match result
883
+ {
884
+ Err(e) if e.to_string().contains("does not exist") => Err(
885
+ StoreError::InvalidFormat(format!(
886
+ "Vector index '{}' does not exist. Create it first with create_vector_index.",
887
+ index_name
888
+ ))
889
+ .into(),
890
+ ),
891
+ other => other.map(|_| ()).map_err(Into::into),
892
+ }
893
+ }
894
+
895
+ fn list_vector_indexes_impl(
896
+ &self,
897
+ transaction: &mut Self::TransactionBox,
898
+ graph_name: &str,
899
+ ) -> Result<Vec<super::store::VectorIndexSpec>>
900
+ {
901
+ let indices: Vec<VectorIndexMetadata> = self
902
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
903
+
904
+ let specs = indices
905
+ .into_iter()
906
+ .map(|m| super::store::VectorIndexSpec {
907
+ name: m.name,
908
+ label: m.label,
909
+ property: m.property,
910
+ dimension: m.dimension,
911
+ metric: m.metric,
912
+ ..Default::default()
913
+ })
914
+ .collect();
915
+
916
+ Ok(specs)
917
+ }
918
+
919
+ /// Search for similar vectors in PostgreSQL
920
+ fn vector_search_impl(
921
+ &self,
922
+ transaction: &mut Self::TransactionBox,
923
+ graph_name: &str,
924
+ index_name: &str,
925
+ query: &crate::value::Tensor,
926
+ k: usize,
927
+ ) -> Result<Vec<super::store::VectorSearchResult>>
928
+ {
929
+ if k == 0
930
+ {
931
+ return Ok(Vec::new());
932
+ }
933
+
934
+ let f32_query: std::borrow::Cow<[f32]> = match query.dtype()
935
+ {
936
+ crate::value::DType::F32 => match query.as_f32_slice()
937
+ {
938
+ Some(s) => std::borrow::Cow::Borrowed(s),
939
+ None => std::borrow::Cow::Owned(query.to_f32_vec()),
940
+ },
941
+ crate::value::DType::F64 => std::borrow::Cow::Owned(query.to_f32_vec()),
942
+ other =>
943
+ {
944
+ return Err(
945
+ StoreError::InvalidFormat(format!(
946
+ "PostgreSQL vector search only accepts F32 or F64 tensors; got {:?}",
947
+ other
948
+ ))
949
+ .into(),
950
+ );
951
+ }
952
+ };
953
+
954
+ if f32_query.iter().any(|v| !v.is_finite())
955
+ {
956
+ return Err(
957
+ StoreError::InvalidFormat("vector contains non-finite value (NaN or Inf)".to_string())
958
+ .into(),
959
+ );
960
+ }
961
+
962
+ let table_name = vector_index_table_name(graph_name, index_name);
963
+
964
+ let indices: Vec<VectorIndexMetadata> = self
965
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
966
+ let metadata = indices
967
+ .into_iter()
968
+ .find(|m| m.name == index_name)
969
+ .ok_or_else(|| {
970
+ StoreError::InvalidFormat(format!(
971
+ "Vector index '{}' does not exist in graph '{}'",
972
+ index_name, graph_name
973
+ ))
974
+ })?;
975
+
976
+ if f32_query.len() != metadata.dimension
977
+ {
978
+ return Err(
979
+ StoreError::InvalidFormat(format!(
980
+ "Vector dimension {} does not match index dimension {}",
981
+ f32_query.len(),
982
+ metadata.dimension
983
+ ))
984
+ .into(),
985
+ );
986
+ }
987
+
988
+ let metric = metadata.metric;
989
+
990
+ if metric == super::VectorMetric::Cosine
991
+ {
992
+ let full_scan_sql = format!("SELECT node_id, embedding::text FROM {}", table_name);
993
+ let client = transaction.get_client();
994
+ let rows = client.query(&full_scan_sql, &[])?;
995
+
996
+ let mut results: Vec<super::store::VectorSearchResult> = rows
997
+ .into_iter()
998
+ .map(|row| {
999
+ let node_uuid: uuid::Uuid = row.get(0);
1000
+ let embedding_text: String = row.get(1);
1001
+ let embedding = parse_pgvector_from_string(&embedding_text)?;
1002
+ let score = cosine_distance(&f32_query, &embedding);
1003
+
1004
+ Ok(super::store::VectorSearchResult {
1005
+ node_id: crate::graph::Key::new(node_uuid.as_u128()),
1006
+ score,
1007
+ })
1008
+ })
1009
+ .collect::<Result<_>>()?;
1010
+
1011
+ results.sort_by(|a, b| {
1012
+ a.score
1013
+ .partial_cmp(&b.score)
1014
+ .unwrap_or(std::cmp::Ordering::Equal)
1015
+ });
1016
+ results.truncate(k);
1017
+ return Ok(results);
1018
+ }
1019
+
1020
+ let operator = metric_to_pgvector_operator(Some(&metric));
1021
+
1022
+ let query_str = serialize_vector_for_pgvector(&f32_query);
1023
+ let search_sql = format!(
1024
+ "SELECT node_id, (embedding {} '{}'::vector) AS score FROM {} ORDER BY embedding {} '{}'::vector LIMIT $1",
1025
+ operator, query_str, table_name, operator, query_str,
1026
+ );
1027
+
1028
+ let client = transaction.get_client();
1029
+ let rows = client.query(&search_sql, &[&(k as i64)])?;
1030
+
1031
+ let results: Vec<super::store::VectorSearchResult> = rows
1032
+ .into_iter()
1033
+ .map(|row| {
1034
+ let node_uuid: uuid::Uuid = row.get(0);
1035
+ // The distance value from pgvector might be a different type
1036
+ // Try to get it as a generic value and convert
1037
+ let score: f32 = match row.try_get::<_, f64>(1)
1038
+ {
1039
+ Ok(val) => val as f32,
1040
+ Err(_) =>
1041
+ {
1042
+ // Try as f32 directly
1043
+ match row.try_get::<_, f32>(1)
1044
+ {
1045
+ Ok(val) => val,
1046
+ Err(_) =>
1047
+ {
1048
+ // Try as i32 or i64
1049
+ match row.try_get::<_, i32>(1)
1050
+ {
1051
+ Ok(val) => val as f32,
1052
+ Err(_) => match row.try_get::<_, i64>(1)
1053
+ {
1054
+ Ok(val) => val as f32,
1055
+ Err(_) => 0.0,
1056
+ },
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+ };
1062
+
1063
+ super::store::VectorSearchResult {
1064
+ node_id: crate::graph::Key::new(node_uuid.as_u128()),
1065
+ score,
1066
+ }
1067
+ })
1068
+ .collect();
1069
+
1070
+ Ok(results)
1071
+ }
1072
+
1073
+ fn drop_vector_index_impl(
1074
+ &self,
1075
+ transaction: &mut Self::TransactionBox,
1076
+ graph_name: &str,
1077
+ index_name: &str,
1078
+ ) -> Result<()>
1079
+ {
1080
+ let mut indices: Vec<VectorIndexMetadata> = self
1081
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
1082
+
1083
+ if !indices.iter().any(|m| m.name == index_name)
1084
+ {
1085
+ return Err(
1086
+ StoreError::InvalidFormat(format!(
1087
+ "Vector index '{}' does not exist in graph '{}'",
1088
+ index_name, graph_name
1089
+ ))
1090
+ .into(),
1091
+ );
1092
+ }
1093
+
1094
+ let table_name = vector_index_table_name(graph_name, index_name);
1095
+ let index_table_sql = format!("DROP TABLE IF EXISTS {}", table_name);
1096
+ transaction.get_client().execute(&index_table_sql, &[])?;
1097
+
1098
+ let hnsw_index_name = format!("idx_{}_{}", graph_name, index_name);
1099
+ let drop_hnsw_index_sql = format!("DROP INDEX IF EXISTS {}", hnsw_index_name);
1100
+ transaction
1101
+ .get_client()
1102
+ .execute(&drop_hnsw_index_sql, &[])?;
1103
+
1104
+ indices.retain(|m| m.name != index_name);
1105
+ self.set_metadata_value_json(
1106
+ transaction,
1107
+ format!("{}/vector_indices", graph_name),
1108
+ &indices,
1109
+ )?;
1110
+
1111
+ Ok(())
1112
+ }
1113
+
1114
+ fn drop_graph_vector_indices_impl(
1115
+ &self,
1116
+ transaction: &mut Self::TransactionBox,
1117
+ graph_name: &str,
1118
+ ) -> Result<()>
1119
+ {
1120
+ let indices_key = format!("{}/vector_indices", graph_name);
1121
+ let indices: Vec<VectorIndexMetadata> =
1122
+ self.get_metadata_value_json_or_default(transaction, &indices_key)?;
1123
+
1124
+ for m in &indices
1125
+ {
1126
+ let table_name = vector_index_table_name(graph_name, &m.name);
1127
+ transaction
1128
+ .get_client()
1129
+ .execute(&format!("DROP TABLE IF EXISTS {}", table_name), &[])?;
1130
+ let hnsw_index_name = format!("idx_{}_{}", graph_name, m.name);
1131
+ transaction
1132
+ .get_client()
1133
+ .execute(&format!("DROP INDEX IF EXISTS {}", hnsw_index_name), &[])?;
1134
+ }
1135
+
1136
+ self.delete_metadata(transaction, &indices_key)?;
1137
+ Ok(())
1138
+ }
1139
+
1140
+ fn get_vector_indices_for_property_impl(
1141
+ &self,
1142
+ transaction: &mut Self::TransactionBox,
1143
+ graph_name: &str,
1144
+ label: &str,
1145
+ property: &str,
1146
+ ) -> Result<Vec<(String, usize, super::store::VectorMetric)>>
1147
+ {
1148
+ let indices: Vec<VectorIndexMetadata> = self
1149
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
1150
+
1151
+ let result = indices
1152
+ .into_iter()
1153
+ .filter(|m| m.label.as_deref() == Some(label) && m.property.as_deref() == Some(property))
1154
+ .map(|m| (m.name, m.dimension, m.metric))
1155
+ .collect();
1156
+
1157
+ Ok(result)
1158
+ }
1159
+
1160
+ fn apply_vector_indices_for_node_impl(
1161
+ &self,
1162
+ transaction: &mut Self::TransactionBox,
1163
+ graph_name: &str,
1164
+ node: &crate::graph::Node,
1165
+ ) -> Result<()>
1166
+ {
1167
+ for label in node.labels()
1168
+ {
1169
+ for property in node.properties().keys()
1170
+ {
1171
+ let Ok(indices) =
1172
+ self.get_vector_indices_for_property_impl(transaction, graph_name, label, property)
1173
+ else
1174
+ {
1175
+ continue;
1176
+ };
1177
+
1178
+ for (index_name, dimension, _metric) in indices
1179
+ {
1180
+ if let Some(val) = node.properties().get(property)
1181
+ && let Some(vector) = crate::store::vector_extract::extract_f32_vec(val)
1182
+ && vector.len() == dimension
1183
+ {
1184
+ let tensor = crate::value::Tensor::from_f32_slice(&vector);
1185
+ let _ =
1186
+ self.upsert_vector_impl(transaction, graph_name, &index_name, node.key(), &tensor);
1187
+ }
1188
+ }
1189
+ }
1190
+ }
1191
+
1192
+ Ok(())
1193
+ }
1194
+
1195
+ fn delete_vectors_for_nodes_impl(
1196
+ &self,
1197
+ transaction: &mut Self::TransactionBox,
1198
+ graph_name: &str,
1199
+ node_ids: &[String],
1200
+ ) -> Result<()>
1201
+ {
1202
+ if node_ids.is_empty()
1203
+ {
1204
+ return Ok(());
1205
+ }
1206
+
1207
+ let indices: Vec<VectorIndexMetadata> = self
1208
+ .get_metadata_value_json_or_default(transaction, format!("{}/vector_indices", graph_name))?;
1209
+
1210
+ for m in &indices
1211
+ {
1212
+ let table_name = vector_index_table_name(graph_name, &m.name);
1213
+ let client = transaction.get_client();
1214
+
1215
+ for node_id in node_ids
1216
+ {
1217
+ let node_uuid = uuid::Uuid::parse_str(&format_hex_to_uuid(node_id))
1218
+ .map_err(|_| StoreError::InvalidFormat(format!("invalid node_id: '{}'", node_id)))?;
1219
+ client.execute(
1220
+ &format!("DELETE FROM {} WHERE node_id = $1", table_name),
1221
+ &[&node_uuid],
1222
+ )?;
1223
+ }
1224
+ }
1225
+
1226
+ Ok(())
1227
+ }
502
1228
  }
503
1229
 
504
1230
  impl super::sqlbase::SqlQueries for Store
@@ -559,7 +1285,7 @@ impl super::sqlbase::SqlQueries for Store
559
1285
  }
560
1286
 
561
1287
  fn node_delete_query(graph_name: impl AsRef<str>, keys: impl AsRef<Vec<String>>)
562
- -> Result<String>
1288
+ -> Result<String>
563
1289
  {
564
1290
  Ok(
565
1291
  templates::NodeDelete {
@@ -608,7 +1334,7 @@ impl super::sqlbase::SqlQueries for Store
608
1334
  )
609
1335
  }
610
1336
  fn edge_delete_query(graph_name: impl AsRef<str>, keys: impl AsRef<Vec<String>>)
611
- -> Result<String>
1337
+ -> Result<String>
612
1338
  {
613
1339
  Ok(
614
1340
  templates::EdgeDelete {