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.
- checksums.yaml +4 -4
- data/ext/Cargo.toml +10 -4
- data/ext/db-index/Cargo.toml +29 -0
- data/ext/db-index/src/hnsw/error.rs +66 -0
- data/ext/db-index/src/hnsw/graph_store.rs +272 -0
- data/ext/db-index/src/hnsw/id.rs +36 -0
- data/ext/db-index/src/hnsw/implementation.rs +1517 -0
- data/ext/db-index/src/hnsw/kernels.rs +1228 -0
- data/ext/db-index/src/hnsw/metric.rs +244 -0
- data/ext/db-index/src/hnsw/scalar.rs +72 -0
- data/ext/db-index/src/hnsw/simple_store.rs +140 -0
- data/ext/db-index/src/hnsw/store.rs +472 -0
- data/ext/db-index/src/hnsw/vector.rs +105 -0
- data/ext/db-index/src/hnsw/vectors.rs +568 -0
- data/ext/db-index/src/hnsw.rs +42 -0
- data/ext/db-index/src/lib.rs +3 -0
- data/ext/gqlitedb/Cargo.toml +19 -9
- data/ext/gqlitedb/benches/common/pokec.rs +62 -2
- data/ext/gqlitedb/benches/pokec_divan.rs +66 -2
- data/ext/gqlitedb/benches/pokec_iai.rs +60 -3
- data/ext/gqlitedb/release.toml +2 -2
- data/ext/gqlitedb/src/aggregators/arithmetic.rs +3 -1
- data/ext/gqlitedb/src/aggregators/containers.rs +1 -1
- data/ext/gqlitedb/src/aggregators/stats.rs +21 -12
- data/ext/gqlitedb/src/capi.rs +1 -1
- data/ext/gqlitedb/src/compiler/expression_analyser.rs +28 -20
- data/ext/gqlitedb/src/compiler/variables_manager.rs +97 -29
- data/ext/gqlitedb/src/compiler.rs +505 -225
- data/ext/gqlitedb/src/connection.rs +149 -11
- data/ext/gqlitedb/src/consts.rs +1 -2
- data/ext/gqlitedb/src/error.rs +123 -61
- data/ext/gqlitedb/src/functions/common.rs +39 -2
- data/ext/gqlitedb/src/functions/math.rs +8 -3
- data/ext/gqlitedb/src/functions/path.rs +48 -11
- data/ext/gqlitedb/src/functions/value.rs +1 -1
- data/ext/gqlitedb/src/functions.rs +23 -7
- data/ext/gqlitedb/src/graph.rs +1 -9
- data/ext/gqlitedb/src/interpreter/evaluators.rs +968 -130
- data/ext/gqlitedb/src/interpreter/instructions.rs +39 -2
- data/ext/gqlitedb/src/lib.rs +5 -4
- data/ext/gqlitedb/src/planner.rs +329 -0
- data/ext/gqlitedb/src/prelude.rs +3 -3
- data/ext/gqlitedb/src/store/pgrx.rs +1 -1
- data/ext/gqlitedb/src/store/postgres.rs +735 -7
- data/ext/gqlitedb/src/store/redb/hnsw_store.rs +702 -0
- data/ext/gqlitedb/src/store/redb/index.rs +274 -0
- data/ext/gqlitedb/src/store/redb.rs +1268 -113
- data/ext/gqlitedb/src/store/sqlbase/sqlmetadata.rs +28 -0
- data/ext/gqlitedb/src/store/sqlbase/sqlstore.rs +103 -0
- data/ext/gqlitedb/src/store/sqlbase.rs +146 -16
- data/ext/gqlitedb/src/store/sqlite.rs +569 -5
- data/ext/gqlitedb/src/store/vector_extract.rs +56 -0
- data/ext/gqlitedb/src/store.rs +123 -3
- data/ext/gqlitedb/src/tests/compiler.rs +207 -10
- data/ext/gqlitedb/src/tests/connection/postgres.rs +38 -3
- data/ext/gqlitedb/src/tests/connection/redb.rs +23 -0
- data/ext/gqlitedb/src/tests/connection/sqlite.rs +31 -0
- data/ext/gqlitedb/src/tests/connection.rs +61 -1
- data/ext/gqlitedb/src/tests/evaluators.rs +162 -7
- data/ext/gqlitedb/src/tests/parser.rs +54 -23
- data/ext/gqlitedb/src/tests/planner.rs +511 -0
- data/ext/gqlitedb/src/tests/store/postgres.rs +7 -0
- data/ext/gqlitedb/src/tests/store/redb.rs +8 -0
- data/ext/gqlitedb/src/tests/store/sqlite.rs +8 -0
- data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +182 -0
- data/ext/gqlitedb/src/tests/store/vector_index/redb.rs +386 -0
- data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +166 -0
- data/ext/gqlitedb/src/tests/store/vector_index.rs +2313 -0
- data/ext/gqlitedb/src/tests/store.rs +78 -14
- data/ext/gqlitedb/src/tests/templates/ast.rs +92 -16
- data/ext/gqlitedb/src/tests/templates/programs.rs +14 -7
- data/ext/gqlitedb/src/tests.rs +15 -9
- data/ext/gqlitedb/src/utils.rs +6 -1
- data/ext/gqlitedb/src/value/compare.rs +61 -3
- data/ext/gqlitedb/src/value.rs +136 -7
- data/ext/gqlitedb/templates/sql/postgres/metadata_delete.sql +1 -0
- data/ext/gqlitedb/templates/sql/postgres/node_select.sql +1 -1
- data/ext/gqlitedb/templates/sql/sqlite/metadata_delete.sql +1 -0
- data/ext/gqliterb/src/lib.rs +53 -8
- data/ext/gqlparser/Cargo.toml +25 -0
- data/ext/gqlparser/README.MD +9 -0
- data/ext/gqlparser/benches/pokec_divan.rs +34 -0
- data/ext/gqlparser/src/common.rs +69 -0
- data/ext/gqlparser/src/gqls/ast.rs +69 -0
- data/ext/gqlparser/src/gqls/constraint.rs +79 -0
- data/ext/gqlparser/src/gqls/error.rs +22 -0
- data/ext/gqlparser/src/gqls/parser.rs +813 -0
- data/ext/gqlparser/src/gqls/prelude.rs +8 -0
- data/ext/gqlparser/src/gqls/properties.rs +115 -0
- data/ext/gqlparser/src/gqls/resolve.rs +207 -0
- data/ext/gqlparser/src/gqls.rs +265 -0
- data/ext/gqlparser/src/lib.rs +7 -0
- data/ext/gqlparser/src/oc/ast.rs +680 -0
- data/ext/gqlparser/src/oc/error.rs +172 -0
- data/ext/gqlparser/src/oc/lexer.rs +429 -0
- data/ext/gqlparser/src/oc/parser/tests.rs +2284 -0
- data/ext/gqlparser/src/oc/parser.rs +2005 -0
- data/ext/gqlparser/src/oc.rs +33 -0
- data/ext/gqlparser/src/prelude.rs +3 -0
- data/ext/graphcore/Cargo.toml +1 -0
- data/ext/graphcore/src/error.rs +26 -0
- data/ext/graphcore/src/graph.rs +177 -51
- data/ext/graphcore/src/lib.rs +4 -2
- data/ext/graphcore/src/open_cypher.rs +12 -0
- data/ext/graphcore/src/table.rs +50 -2
- data/ext/graphcore/src/timestamp.rs +127 -104
- data/ext/graphcore/src/value/tensor.rs +739 -0
- data/ext/graphcore/src/value/value_map.rs +1 -1
- data/ext/graphcore/src/value.rs +343 -19
- metadata +90 -22
- data/ext/gqlitedb/src/parser/ast.rs +0 -604
- data/ext/gqlitedb/src/parser/parser_impl.rs +0 -1213
- data/ext/gqlitedb/src/parser.rs +0 -4
|
@@ -1,11 +1,12 @@
|
|
|
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::{
|
|
8
|
-
TransactionBoxable,
|
|
9
|
+
TransactionBoxable, VectorMetric,
|
|
9
10
|
sqlbase::{SqlBindingValue, SqlResultValue},
|
|
10
11
|
},
|
|
11
12
|
};
|
|
@@ -17,6 +18,88 @@ 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
|
-
_ =>
|
|
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
|
}
|
|
@@ -128,7 +232,7 @@ impl Drop for TransactionBase
|
|
|
128
232
|
if *self.active.borrow()
|
|
129
233
|
&& let Err(e) = self.client.query("ROLLBACK", &[])
|
|
130
234
|
{
|
|
131
|
-
|
|
235
|
+
log::error!(
|
|
132
236
|
"Rollback failed with error {:?}, future use of the connection are likely to fail.",
|
|
133
237
|
e
|
|
134
238
|
);
|
|
@@ -305,6 +409,16 @@ mod templates
|
|
|
305
409
|
}
|
|
306
410
|
}
|
|
307
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
|
+
|
|
308
422
|
// ____ _
|
|
309
423
|
// / ___|| |_ ___ _ __ ___
|
|
310
424
|
// \___ \| __/ _ \| '__/ _ \
|
|
@@ -328,16 +442,103 @@ impl Store
|
|
|
328
442
|
move || Ok(config.connect(NoTls)?),
|
|
329
443
|
pool::Options::default().minimum_pool_size(1).pool_size(3),
|
|
330
444
|
)?;
|
|
445
|
+
{
|
|
446
|
+
let mut setup_client = client.get()?;
|
|
447
|
+
setup_client.execute("CREATE EXTENSION IF NOT EXISTS vector", &[])?;
|
|
448
|
+
}
|
|
331
449
|
|
|
332
450
|
let s = Self { client };
|
|
333
451
|
s.initialise()?;
|
|
334
452
|
Ok(s)
|
|
335
453
|
}
|
|
336
|
-
fn upgrade_database(&self,
|
|
337
|
-
-> Result<()>
|
|
454
|
+
fn upgrade_database(&self, transaction: &mut TransactionBox, from: utils::Version) -> Result<()>
|
|
338
455
|
{
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
}
|
|
341
542
|
}
|
|
342
543
|
/// Check if table exists
|
|
343
544
|
pub(crate) fn check_if_table_exists(
|
|
@@ -366,6 +567,10 @@ impl super::sqlbase::SqlMetaDataQueries for Store
|
|
|
366
567
|
{
|
|
367
568
|
Ok(include_str!("../../templates/sql/postgres/metadata_set.sql").to_string())
|
|
368
569
|
}
|
|
570
|
+
fn metadata_delete_query() -> Result<String>
|
|
571
|
+
{
|
|
572
|
+
Ok(include_str!("../../templates/sql/postgres/metadata_delete.sql").to_string())
|
|
573
|
+
}
|
|
369
574
|
}
|
|
370
575
|
|
|
371
576
|
impl super::sqlbase::SqlStore for Store
|
|
@@ -497,6 +702,529 @@ impl super::sqlbase::SqlStore for Store
|
|
|
497
702
|
}
|
|
498
703
|
Ok(())
|
|
499
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
|
+
}
|
|
500
1228
|
}
|
|
501
1229
|
|
|
502
1230
|
impl super::sqlbase::SqlQueries for Store
|