gqlite 1.8.0 → 1.8.2

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +8 -8
  3. data/ext/db-index/Cargo.toml +2 -2
  4. data/ext/gqlitedb/Cargo.toml +0 -2
  5. data/ext/gqlitedb/src/aggregators.rs +28 -0
  6. data/ext/gqlitedb/src/compiler/expression_analyser.rs +33 -0
  7. data/ext/gqlitedb/src/compiler/variables_manager.rs +111 -11
  8. data/ext/gqlitedb/src/compiler.rs +506 -48
  9. data/ext/gqlitedb/src/connection.rs +14 -2
  10. data/ext/gqlitedb/src/consts.rs +1 -1
  11. data/ext/gqlitedb/src/error.rs +31 -0
  12. data/ext/gqlitedb/src/interpreter/evaluators.rs +153 -331
  13. data/ext/gqlitedb/src/interpreter/instructions.rs +77 -0
  14. data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
  15. data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
  16. data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
  17. data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
  18. data/ext/gqlitedb/src/interpreter/path.rs +542 -0
  19. data/ext/gqlitedb/src/planner.rs +231 -6
  20. data/ext/gqlitedb/src/query_result.rs +19 -4
  21. data/ext/gqlitedb/src/store/redb.rs +1 -1
  22. data/ext/gqlitedb/src/tests/compiler.rs +2454 -1
  23. data/ext/gqlitedb/src/tests/connection/postgres.rs +22 -0
  24. data/ext/gqlitedb/src/tests/connection/redb.rs +24 -0
  25. data/ext/gqlitedb/src/tests/connection/sqlite.rs +16 -0
  26. data/ext/gqlitedb/src/tests/connection.rs +101 -0
  27. data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
  28. data/ext/gqlitedb/src/tests/parser.rs +1225 -0
  29. data/ext/gqlitedb/src/tests/planner.rs +433 -2
  30. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
  31. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
  32. data/ext/gqlitedb/src/tests/templates/ast.rs +294 -1
  33. data/ext/gqlitedb/src/tests/templates/programs.rs +318 -0
  34. data/ext/gqliterb/Cargo.toml +0 -1
  35. data/ext/gqlparser/src/oc/ast.rs +72 -3
  36. data/ext/gqlparser/src/oc/error.rs +18 -0
  37. data/ext/gqlparser/src/oc/lexer.rs +157 -2
  38. data/ext/gqlparser/src/oc/parser/tests.rs +735 -29
  39. data/ext/gqlparser/src/oc/parser.rs +1312 -182
  40. data/ext/gqlparser/src/oc/string.rs +172 -0
  41. data/ext/gqlparser/src/oc.rs +1 -0
  42. data/ext/graphcore/src/value/value_map.rs +2 -2
  43. data/ext/graphcore/src/value.rs +13 -1
  44. metadata +6 -2
  45. data/ext/gqlitedb/src/parser/gql.pest +0 -198
@@ -0,0 +1,542 @@
1
+ use rustc_hash::FxHashSet;
2
+
3
+ use crate::{
4
+ graph,
5
+ interpreter::{
6
+ evaluators::{Stack, TryPopInto, eval_instructions},
7
+ instructions,
8
+ },
9
+ prelude::*,
10
+ store,
11
+ value::Value,
12
+ value_table::{self, MutableRowInterface, RowInterface},
13
+ };
14
+
15
+ pub(crate) mod bfs;
16
+ pub(crate) mod dfs;
17
+ pub(crate) mod group;
18
+
19
+ /// Extract every Edge-typed value bound among `group_variables` on `row`.
20
+ /// Used by both traversal strategies to determine which edges one
21
+ /// repetition consumed, for trail-uniqueness bookkeeping.
22
+ pub(crate) fn extract_edges(
23
+ row: &value_table::Row,
24
+ group_variables: &[value_table::ColId],
25
+ ) -> Vec<graph::Key>
26
+ {
27
+ group_variables
28
+ .iter()
29
+ .filter_map(|col| row.get(*col).ok())
30
+ .filter_map(|val| match val
31
+ {
32
+ Value::Edge(e) => Some(e.key()),
33
+ _ => None,
34
+ })
35
+ .collect()
36
+ }
37
+
38
+ /// Resolves start nodes via `source_instructions`, then filters them
39
+ /// through `source_filter` if present. Identical logic regardless of
40
+ /// which traversal strategy follows.
41
+ #[allow(clippy::too_many_arguments)]
42
+ pub(crate) fn resolve_start_nodes<TStore: store::Store>(
43
+ source_instructions: &instructions::Instructions,
44
+ source_variable: &Option<value_table::ColId>,
45
+ source_filter: &instructions::Instructions,
46
+ store: &TStore,
47
+ tx: &mut TStore::TransactionBox,
48
+ graph_name: &str,
49
+ parameters: &crate::value::ValueMap,
50
+ row: &value_table::Row,
51
+ ) -> Result<Vec<graph::Node>>
52
+ {
53
+ let mut start_nodes = Vec::new();
54
+
55
+ if !source_instructions.is_empty()
56
+ {
57
+ let mut eval_stack = Stack::default();
58
+ eval_instructions(&mut eval_stack, row, source_instructions, parameters)?;
59
+ let query: store::SelectNodeQuery = eval_stack.try_pop_into()?;
60
+ start_nodes = store.select_nodes(tx, graph_name, query)?;
61
+ }
62
+ else if let Some(src_col) = source_variable
63
+ && let Value::Node(node) = row.get(*src_col)?
64
+ {
65
+ start_nodes.push(node.clone())
66
+ }
67
+
68
+ // Apply source_filter to each start node
69
+ if !source_filter.is_empty()
70
+ {
71
+ start_nodes.retain(|node| {
72
+ let mut test_row = row.clone();
73
+ if let Some(sv) = source_variable
74
+ && let Err(_) = test_row.set(*sv, node.clone().into())
75
+ {
76
+ return false;
77
+ }
78
+ let mut eval_stack = Stack::default();
79
+ match eval_instructions(&mut eval_stack, &test_row, source_filter, parameters)
80
+ {
81
+ Ok(_) => eval_stack.try_pop_into().unwrap_or(false),
82
+ Err(_) => false,
83
+ }
84
+ });
85
+ }
86
+
87
+ Ok(start_nodes)
88
+ }
89
+
90
+ /// Checks `destination_filter` against `row`, if present.
91
+ /// Decision made here, not left inconsistent: a filter-evaluation
92
+ /// error is propagated (`?`), not silently treated as "doesn't pass" -
93
+ /// matching `bfs.rs`'s existing choice, not `group.rs`'s
94
+ /// `.unwrap_or(false)`. Swallowing an evaluation error into a silent
95
+ /// non-match would hide a real bug behind what looks like an ordinary
96
+ /// filtered-out candidate.
97
+ pub(crate) fn check_destination_filter(
98
+ destination_filter: &instructions::Instructions,
99
+ row: &value_table::Row,
100
+ parameters: &crate::value::ValueMap,
101
+ ) -> Result<bool>
102
+ {
103
+ if destination_filter.is_empty()
104
+ {
105
+ return Ok(true);
106
+ }
107
+ let mut eval_stack = Stack::default();
108
+ eval_instructions(&mut eval_stack, row, destination_filter, parameters)?;
109
+ eval_stack.try_pop_into()
110
+ }
111
+
112
+ /// Binds the destination node and assembles the `path_variable`'s
113
+ /// `graph::Path`, reading scalar values from `candidate_row` - which
114
+ /// must be the row as returned from `execute_block_matches` for this
115
+ /// repetition, before `group_variables` columns are overwritten with
116
+ /// their accumulated arrays. This is the exact logic that diverged
117
+ /// between `group.rs` (correct) and `bfs.rs` (still using a
118
+ /// first-node-found heuristic) - extracting it removes the possibility
119
+ /// of that divergence happening again.
120
+ pub(crate) fn bind_destination_and_path(
121
+ candidate_row: &value_table::Row,
122
+ group_terminal_variable: value_table::ColId,
123
+ group_variables: &[value_table::ColId],
124
+ path_edges: &[graph::Edge],
125
+ start_node: &graph::Node,
126
+ ) -> (Option<graph::Node>, Option<graph::Path>)
127
+ {
128
+ let terminal_node = match candidate_row.get(group_terminal_variable)
129
+ {
130
+ Ok(Value::Node(node)) => Some(node.clone()),
131
+ _ => None,
132
+ };
133
+
134
+ let path = if !path_edges.is_empty()
135
+ {
136
+ let mut nodes = vec![start_node.clone()];
137
+ for var_col in group_variables
138
+ {
139
+ if let Ok(Value::Node(node)) = candidate_row.get(*var_col)
140
+ {
141
+ nodes.push(node.clone());
142
+ }
143
+ }
144
+ let path = if nodes.len() > 1
145
+ {
146
+ graph::Path::new(
147
+ start_node.clone(),
148
+ path_edges
149
+ .iter()
150
+ .zip(nodes.iter().skip(1))
151
+ .map(|(e, n)| (e.clone(), n.clone()))
152
+ .collect(),
153
+ )
154
+ }
155
+ else
156
+ {
157
+ graph::Path::new(start_node.clone(), vec![])
158
+ };
159
+ Some(path)
160
+ }
161
+ else
162
+ {
163
+ None
164
+ };
165
+
166
+ (terminal_node, path)
167
+ }
168
+
169
+ /// can be reused within a single path traversal. `Trail` ensures no edge is
170
+ /// repeated (the only implemented variant).
171
+ #[derive(Debug, Clone, Copy)]
172
+ pub(crate) enum PathUniqueness
173
+ {
174
+ /// Trail uniqueness: no edge may be repeated within a single path.
175
+ /// This is the standard constraint for Cypher path expressions.
176
+ Trail,
177
+ }
178
+
179
+ impl PathUniqueness
180
+ {
181
+ /// Check if an edge can be traversed given the current set of used edges.
182
+ /// For `Trail`, returns true iff the edge has not been used yet.
183
+ pub(crate) fn is_reusable(
184
+ &self,
185
+ used_edges: &FxHashSet<graph::Key>,
186
+ edge_key: &graph::Key,
187
+ ) -> bool
188
+ {
189
+ match self
190
+ {
191
+ PathUniqueness::Trail => !used_edges.contains(edge_key),
192
+ }
193
+ }
194
+
195
+ /// Mark an edge as used in the current path traversal.
196
+ /// For `Trail`, inserts the edge into the used set.
197
+ pub(crate) fn mark_used(&self, used_edges: &mut FxHashSet<graph::Key>, edge_key: graph::Key)
198
+ {
199
+ match self
200
+ {
201
+ PathUniqueness::Trail =>
202
+ {
203
+ used_edges.insert(edge_key);
204
+ }
205
+ }
206
+ }
207
+
208
+ /// Mark an edge as unused when backtracking out of a branch.
209
+ /// For `Trail`, removes the edge from the used set for future branches.
210
+ pub(crate) fn mark_unused(&self, used_edges: &mut FxHashSet<graph::Key>, edge_key: &graph::Key)
211
+ {
212
+ match self
213
+ {
214
+ PathUniqueness::Trail =>
215
+ {
216
+ used_edges.remove(edge_key);
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ /// Executes a `BlockMatch::MatchEdge` against a single row, appending any
223
+ /// resulting rows into `new_rows`.
224
+ #[allow(clippy::too_many_arguments)]
225
+ pub(crate) fn execute_match_edge<TStore: store::Store>(
226
+ block: &instructions::BlockMatch,
227
+ store: &TStore,
228
+ tx: &mut TStore::TransactionBox,
229
+ graph_name: &str,
230
+ stack: &mut Stack,
231
+ parameters: &crate::value::ValueMap,
232
+ row: &value_table::Row,
233
+ allow_recursive: bool,
234
+ new_rows: &mut Vec<value_table::Row>,
235
+ ) -> Result<()>
236
+ {
237
+ let instructions::BlockMatch::MatchEdge {
238
+ instructions,
239
+ left_variable,
240
+ edge_variable,
241
+ right_variable,
242
+ path_variable,
243
+ filter,
244
+ directivity,
245
+ recursive_range,
246
+ } = block
247
+ else
248
+ {
249
+ unreachable!("execute_match_edge called with a non-MatchEdge block");
250
+ };
251
+
252
+ eval_instructions(stack, row, instructions, parameters)?;
253
+ let query = stack.try_pop_into()?;
254
+
255
+ match recursive_range
256
+ {
257
+ None =>
258
+ {
259
+ let edges = store.select_edges(tx, graph_name, query, *directivity)?;
260
+
261
+ for edge in edges.into_iter()
262
+ {
263
+ let mut new_row = row.clone();
264
+ let (left, right) = if edge.reversed
265
+ {
266
+ (edge.path.destination(), edge.path.source())
267
+ }
268
+ else
269
+ {
270
+ (edge.path.source(), edge.path.destination())
271
+ };
272
+
273
+ if let Some(left_variable) = left_variable
274
+ {
275
+ new_row.set(*left_variable, left.to_owned().into())?;
276
+ }
277
+ if let Some(right_variable) = right_variable
278
+ {
279
+ new_row.set(*right_variable, right.to_owned().into())?;
280
+ }
281
+ if let Some(edge_variable) = edge_variable
282
+ {
283
+ new_row.set(*edge_variable, edge.path.to_edge().into())?;
284
+ }
285
+ if let Some(path_variable) = path_variable
286
+ {
287
+ new_row.set(*path_variable, edge.path.to_owned().into())?;
288
+ }
289
+
290
+ let should_add_row = if filter.is_empty()
291
+ {
292
+ true
293
+ }
294
+ else
295
+ {
296
+ let mut stack = Stack::default();
297
+ stack.push(true);
298
+ stack.push(edge.path.to_edge());
299
+ eval_instructions(&mut stack, &new_row, filter, parameters)?;
300
+ stack.try_pop()?;
301
+ stack.try_pop_into()?
302
+ };
303
+
304
+ if should_add_row
305
+ {
306
+ new_rows.push(new_row);
307
+ }
308
+ }
309
+ }
310
+ Some(recursive_range) =>
311
+ {
312
+ dfs::execute(
313
+ recursive_range,
314
+ &query,
315
+ left_variable,
316
+ edge_variable,
317
+ right_variable,
318
+ path_variable,
319
+ filter,
320
+ *directivity,
321
+ store,
322
+ tx,
323
+ graph_name,
324
+ parameters,
325
+ row,
326
+ allow_recursive,
327
+ new_rows,
328
+ PathUniqueness::Trail,
329
+ )?;
330
+ }
331
+ }
332
+
333
+ Ok(())
334
+ }
335
+
336
+ /// Dispatch entry point for `MatchQuantifiedPath` execution.
337
+ /// Routes to the appropriate strategy handler (list-capture/DFS, BFS, etc.)
338
+ /// based on the `strategy` field.
339
+ ///
340
+ /// This is called from `execute_block_match` when encountering a `MatchQuantifiedPath` block.
341
+ #[allow(clippy::too_many_arguments)]
342
+ pub(crate) fn execute_match_quantified_path<TStore: store::Store>(
343
+ block: &instructions::BlockMatch,
344
+ store: &TStore,
345
+ tx: &mut TStore::TransactionBox,
346
+ graph_name: &str,
347
+ stack: &mut Stack,
348
+ parameters: &crate::value::ValueMap,
349
+ row: &value_table::Row,
350
+ allow_recursive: bool,
351
+ new_rows: &mut Vec<value_table::Row>,
352
+ ) -> Result<()>
353
+ {
354
+ let instructions::BlockMatch::MatchQuantifiedPath { strategy, .. } = block
355
+ else
356
+ {
357
+ unreachable!("execute_match_quantified_path called with a non-MatchQuantifiedPath block");
358
+ };
359
+ match strategy
360
+ {
361
+ instructions::PathStrategy::Dfs => group::execute(
362
+ block,
363
+ store,
364
+ tx,
365
+ graph_name,
366
+ stack,
367
+ parameters,
368
+ row,
369
+ allow_recursive,
370
+ new_rows,
371
+ ),
372
+ instructions::PathStrategy::Bfs { .. } => bfs::execute(
373
+ block,
374
+ store,
375
+ tx,
376
+ graph_name,
377
+ stack,
378
+ parameters,
379
+ row,
380
+ allow_recursive,
381
+ new_rows,
382
+ ),
383
+ }
384
+ }
385
+
386
+ #[cfg(test)]
387
+ mod tests
388
+ {
389
+ use super::*;
390
+ use crate::value::Value;
391
+ use crate::value_table::{ColId, MutableRowInterface, Row};
392
+
393
+ #[test]
394
+ fn test_extract_edges_mixed_types()
395
+ {
396
+ // Create a row with capacity for 4 columns
397
+ let mut row = Row::new(vec![], 4);
398
+ let col_edge1 = ColId::from(0usize);
399
+ let col_edge2 = ColId::from(1usize);
400
+ let col_node = ColId::from(2usize);
401
+ let col_other = ColId::from(3usize);
402
+
403
+ // Create test edges and nodes using default constructors
404
+ let edge1 = graph::Edge::new(None, Default::default(), vec![], Default::default());
405
+ let edge2 = graph::Edge::new(None, Default::default(), vec![], Default::default());
406
+ let node = graph::Node::new(None, Default::default(), vec![], Default::default());
407
+
408
+ // Set values in row
409
+ row.set(col_edge1, Value::Edge(edge1.clone())).unwrap();
410
+ row.set(col_edge2, Value::Edge(edge2.clone())).unwrap();
411
+ row.set(col_node, Value::Node(node)).unwrap();
412
+ row.set(col_other, Value::Null).unwrap();
413
+
414
+ let group_variables = vec![col_edge1, col_edge2, col_node, col_other];
415
+ let edges = extract_edges(&row, &group_variables);
416
+
417
+ // Should only extract the two edges
418
+ assert_eq!(edges.len(), 2);
419
+ }
420
+
421
+ #[test]
422
+ fn test_extract_edges_no_edges()
423
+ {
424
+ let mut row = Row::new(vec![], 2);
425
+ let col_node = ColId::from(0usize);
426
+ let col_other = ColId::from(1usize);
427
+
428
+ let node = graph::Node::new(None, Default::default(), vec![], Default::default());
429
+ row.set(col_node, Value::Node(node)).unwrap();
430
+ row.set(col_other, Value::Null).unwrap();
431
+
432
+ let group_variables = vec![col_node, col_other];
433
+ let edges = extract_edges(&row, &group_variables);
434
+
435
+ assert_eq!(edges.len(), 0);
436
+ }
437
+
438
+ #[test]
439
+ fn test_check_destination_filter_empty_filter()
440
+ {
441
+ let row = Row::new(vec![], 0);
442
+ let empty_filter = instructions::Instructions::default();
443
+ let params = Default::default();
444
+
445
+ let result = check_destination_filter(&empty_filter, &row, &params).unwrap();
446
+ assert!(result, "Empty filter should always return true");
447
+ }
448
+
449
+ #[test]
450
+ fn test_bind_destination_and_path_with_edges()
451
+ {
452
+ let mut candidate_row = Row::new(vec![], 2);
453
+ let group_term_col = ColId::from(0usize);
454
+ let group_var_col = ColId::from(1usize);
455
+
456
+ let terminal_node = graph::Node::new(None, Default::default(), vec![], Default::default());
457
+ let intermediate_node = graph::Node::new(None, Default::default(), vec![], Default::default());
458
+ let start_node = graph::Node::new(None, Default::default(), vec![], Default::default());
459
+
460
+ candidate_row
461
+ .set(group_term_col, Value::Node(terminal_node.clone()))
462
+ .unwrap();
463
+ candidate_row
464
+ .set(group_var_col, Value::Node(intermediate_node.clone()))
465
+ .unwrap();
466
+
467
+ let edge = graph::Edge::new(None, Default::default(), vec![], Default::default());
468
+
469
+ let group_variables = vec![group_var_col];
470
+ let path_edges = vec![edge.clone()];
471
+
472
+ let (dest_node, path) = bind_destination_and_path(
473
+ &candidate_row,
474
+ group_term_col,
475
+ &group_variables,
476
+ &path_edges,
477
+ &start_node,
478
+ );
479
+
480
+ assert_eq!(dest_node.unwrap().key(), terminal_node.key());
481
+ assert!(path.is_some());
482
+ }
483
+
484
+ #[test]
485
+ fn test_bind_destination_and_path_no_edges()
486
+ {
487
+ let mut candidate_row = Row::new(vec![], 1);
488
+ let group_term_col = ColId::from(0usize);
489
+
490
+ let terminal_node = graph::Node::new(None, Default::default(), vec![], Default::default());
491
+ let start_node = graph::Node::new(None, Default::default(), vec![], Default::default());
492
+
493
+ candidate_row
494
+ .set(group_term_col, Value::Node(terminal_node.clone()))
495
+ .unwrap();
496
+
497
+ let group_variables = vec![];
498
+ let path_edges = vec![];
499
+
500
+ let (dest_node, path) = bind_destination_and_path(
501
+ &candidate_row,
502
+ group_term_col,
503
+ &group_variables,
504
+ &path_edges,
505
+ &start_node,
506
+ );
507
+
508
+ assert_eq!(dest_node.unwrap().key(), terminal_node.key());
509
+ assert!(
510
+ path.is_none(),
511
+ "Path should be None when path_edges is empty"
512
+ );
513
+ }
514
+
515
+ #[test]
516
+ fn test_bind_destination_and_path_missing_terminal()
517
+ {
518
+ let candidate_row = Row::new(vec![], 1);
519
+ let group_term_col = ColId::from(0usize);
520
+ let start_node = graph::Node::new(None, Default::default(), vec![], Default::default());
521
+
522
+ let group_variables = vec![];
523
+ let path_edges = vec![];
524
+
525
+ let (dest_node, path) = bind_destination_and_path(
526
+ &candidate_row,
527
+ group_term_col,
528
+ &group_variables,
529
+ &path_edges,
530
+ &start_node,
531
+ );
532
+
533
+ assert!(
534
+ dest_node.is_none(),
535
+ "Destination node should be None if not set"
536
+ );
537
+ assert!(
538
+ path.is_none(),
539
+ "Path should be None when path_edges is empty"
540
+ );
541
+ }
542
+ }