gqlite 1.8.1 → 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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +8 -8
  3. data/ext/gqlitedb/Cargo.toml +0 -2
  4. data/ext/gqlitedb/src/compiler/variables_manager.rs +109 -10
  5. data/ext/gqlitedb/src/compiler.rs +481 -47
  6. data/ext/gqlitedb/src/connection.rs +12 -0
  7. data/ext/gqlitedb/src/consts.rs +1 -1
  8. data/ext/gqlitedb/src/error.rs +24 -0
  9. data/ext/gqlitedb/src/interpreter/evaluators.rs +91 -325
  10. data/ext/gqlitedb/src/interpreter/instructions.rs +72 -0
  11. data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
  12. data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
  13. data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
  14. data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
  15. data/ext/gqlitedb/src/interpreter/path.rs +542 -0
  16. data/ext/gqlitedb/src/planner.rs +218 -6
  17. data/ext/gqlitedb/src/query_result.rs +19 -4
  18. data/ext/gqlitedb/src/tests/compiler.rs +2453 -1
  19. data/ext/gqlitedb/src/tests/connection/redb.rs +8 -0
  20. data/ext/gqlitedb/src/tests/connection.rs +37 -0
  21. data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
  22. data/ext/gqlitedb/src/tests/parser.rs +1225 -0
  23. data/ext/gqlitedb/src/tests/planner.rs +432 -2
  24. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
  25. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
  26. data/ext/gqlitedb/src/tests/templates/ast.rs +284 -1
  27. data/ext/gqlitedb/src/tests/templates/programs.rs +306 -0
  28. data/ext/gqliterb/Cargo.toml +0 -1
  29. data/ext/gqlparser/src/oc/ast.rs +52 -3
  30. data/ext/gqlparser/src/oc/error.rs +18 -0
  31. data/ext/gqlparser/src/oc/lexer.rs +146 -0
  32. data/ext/gqlparser/src/oc/parser/tests.rs +365 -20
  33. data/ext/gqlparser/src/oc/parser.rs +1196 -143
  34. data/ext/graphcore/src/value.rs +12 -1
  35. metadata +5 -2
  36. data/ext/gqlitedb/src/parser/gql.pest +0 -198
@@ -0,0 +1,494 @@
1
+ use rustc_hash::FxHashSet;
2
+ use std::collections::VecDeque;
3
+
4
+ use crate::{
5
+ graph,
6
+ interpreter::{
7
+ evaluators::{Stack, TryPopInto, eval_instructions},
8
+ instructions,
9
+ path::{self, PathUniqueness},
10
+ },
11
+ prelude::*,
12
+ store,
13
+ value_table::{self, MutableRowInterface, RowInterface},
14
+ };
15
+
16
+ /// State representing one position in the BFS frontier at a given depth.
17
+ #[derive(Clone)]
18
+ struct FrontierNode
19
+ {
20
+ row: value_table::Row,
21
+ depth: usize,
22
+ path_edges: Vec<graph::Edge>,
23
+ group_lists: Vec<Vec<value::Value>>,
24
+ used_edges: FxHashSet<graph::Key>,
25
+ }
26
+
27
+ /// Execute a quantified path pattern using BFS (breadth-first search) shortest-path strategy.
28
+ ///
29
+ /// BFS expands a frontier layer-by-layer by hop distance, stopping when `stop_condition` is satisfied:
30
+ /// - `Shortest(k)`: Stop once k paths reaching destination-filter-passing nodes are found
31
+ /// - `ShortestGroups(k)`: Expand until k distinct depth layers fully drained
32
+ ///
33
+ /// Design decisions (WI#673):
34
+ /// 1. Frontier expansion: Layer-by-layer by hop distance
35
+ /// 2. Stop conditions: Support both SHORTEST and SHORTEST GROUPS variants
36
+ /// 3. path_filter: Applied when candidate reaches destination, before admission
37
+ /// 4. group_where_filter: Applied before accepting candidate (same as DFS)
38
+ #[allow(clippy::too_many_arguments)]
39
+ pub(crate) fn execute<TStore: store::Store>(
40
+ block: &instructions::BlockMatch,
41
+ store: &TStore,
42
+ tx: &mut TStore::TransactionBox,
43
+ graph_name: &str,
44
+ stack: &mut Stack,
45
+ parameters: &crate::value::ValueMap,
46
+ row: &value_table::Row,
47
+ allow_recursive: bool,
48
+ new_rows: &mut Vec<value_table::Row>,
49
+ ) -> Result<()>
50
+ {
51
+ let instructions::BlockMatch::MatchQuantifiedPath {
52
+ source_instructions,
53
+ source_variable,
54
+ source_filter,
55
+ destination_variable,
56
+ destination_filter,
57
+ path_variable,
58
+ quantifier,
59
+ group_instructions,
60
+ group_variables,
61
+ group_terminal_variable,
62
+ group_entry_variable,
63
+ group_where_filter,
64
+ path_filter,
65
+ strategy,
66
+ } = block
67
+ else
68
+ {
69
+ unreachable!("execute called with a non-MatchQuantifiedPath block");
70
+ };
71
+
72
+ if !allow_recursive
73
+ {
74
+ return Err(InternalError::UnsupportedRecursivePatternInMerge.into());
75
+ }
76
+
77
+ let min_hops = quantifier.start;
78
+ let max_hops = quantifier.end;
79
+
80
+ // Extract stop condition
81
+ let (stop_condition, is_groups_mode) = match strategy
82
+ {
83
+ instructions::PathStrategy::Bfs { stop_condition } => match stop_condition
84
+ {
85
+ instructions::StopCondition::Shortest(k_instructions) => (k_instructions, false),
86
+ instructions::StopCondition::ShortestGroups(k_instructions) => (k_instructions, true),
87
+ },
88
+ _ => unreachable!("execute called with non-Bfs strategy"),
89
+ };
90
+
91
+ // Resolve k from stop_condition
92
+ let k: usize = if !stop_condition.is_empty()
93
+ {
94
+ let mut eval_stack = Stack::default();
95
+ eval_instructions(&mut eval_stack, row, stop_condition, parameters)?;
96
+ let k_val: i64 = eval_stack.try_pop_into()?;
97
+ if k_val <= 0
98
+ {
99
+ return Err(Error::RunTime(RunTimeError::InvalidArgument {
100
+ function_name: "SHORTEST",
101
+ index: 0,
102
+ expected_type: "positive integer",
103
+ value: k_val.to_string(),
104
+ }));
105
+ }
106
+ k_val as usize
107
+ }
108
+ else
109
+ {
110
+ 1 // Default to 1 if not specified
111
+ };
112
+
113
+ let capacity = max_hops.min(100);
114
+ let uniqueness = PathUniqueness::Trail;
115
+
116
+ // Resolve start nodes via shared function
117
+ let start_nodes = path::resolve_start_nodes(
118
+ source_instructions,
119
+ source_variable,
120
+ source_filter,
121
+ store,
122
+ tx,
123
+ graph_name,
124
+ parameters,
125
+ row,
126
+ )?;
127
+
128
+ for start_node in start_nodes
129
+ {
130
+ // BFS frontier: queue of (position, depth)
131
+ let mut frontier: VecDeque<FrontierNode> = VecDeque::new();
132
+
133
+ // Initialize with starting node(s) from executing group_instructions once
134
+ let mut position_row = row.clone();
135
+ if let Some(sv) = source_variable
136
+ {
137
+ position_row.set(*sv, start_node.clone().into())?;
138
+ }
139
+ // For the first iteration, set the group's entry point to the start node
140
+ position_row.set(*group_entry_variable, start_node.clone().into())?;
141
+
142
+ let initial_candidates = crate::interpreter::evaluators::execute_block_matches(
143
+ group_instructions,
144
+ store,
145
+ tx,
146
+ graph_name,
147
+ stack,
148
+ parameters,
149
+ vec![position_row],
150
+ allow_recursive,
151
+ )?;
152
+
153
+ // Add initial candidates to frontier at depth 1 (one real repetition
154
+ // already completed by the group_instructions execution above - not
155
+ // zero. Matches dfs.rs's convention: depth means hops taken so far.
156
+ for initial_row in initial_candidates
157
+ {
158
+ // Extract edges and populate group_lists from the initial row
159
+ let mut initial_path_edges = Vec::with_capacity(capacity);
160
+ let mut initial_group_lists = vec![Vec::with_capacity(capacity); group_variables.len()];
161
+
162
+ for (var_idx, var_col) in group_variables.iter().enumerate()
163
+ {
164
+ if let Ok(val) = initial_row.get(*var_col)
165
+ {
166
+ initial_group_lists[var_idx].push(val.clone());
167
+
168
+ // Add edge to path_edges if this value is an edge
169
+ if let crate::value::Value::Edge(edge) = val
170
+ {
171
+ initial_path_edges.push(edge.clone());
172
+ }
173
+ }
174
+ }
175
+
176
+ frontier.push_back(FrontierNode {
177
+ row: initial_row,
178
+ depth: 1,
179
+ path_edges: initial_path_edges,
180
+ group_lists: initial_group_lists,
181
+ used_edges: FxHashSet::with_capacity_and_hasher(capacity, Default::default()),
182
+ });
183
+ }
184
+
185
+ let mut found_paths = 0;
186
+ let mut depths_completed = 0;
187
+ let mut current_depth = 0;
188
+ let mut processing_depth = 0;
189
+ // Track if the current depth produced at least one admitted row (WI#693)
190
+ let mut current_depth_admitted_rows = false;
191
+
192
+ // Zero-repetition case: if min_hops == 0, the start node itself,
193
+ // with no edges traversed, is a valid match - nothing else in this
194
+ // function ever considers this, since initial_candidates always
195
+ // represents one repetition already completed.
196
+ if min_hops == 0
197
+ {
198
+ let mut zero_row = row.clone();
199
+ if let Some(sv) = source_variable
200
+ {
201
+ zero_row.set(*sv, start_node.clone().into())?;
202
+ }
203
+ for var_col in group_variables
204
+ {
205
+ zero_row.set(*var_col, value::Value::Array(Vec::new()))?;
206
+ }
207
+ if let Some(dv) = destination_variable
208
+ {
209
+ zero_row.set(*dv, start_node.clone().into())?;
210
+ }
211
+ if let Some(pv) = path_variable
212
+ {
213
+ zero_row.set(*pv, graph::Path::new(start_node.clone(), vec![]).into())?;
214
+ }
215
+
216
+ // When destination_variable refers back to source_variable (like pattern "(a) ... (a)"),
217
+ // the compiler skips building a destination_filter (since the variable is already set).
218
+ // We must explicitly verify they bind to the same node.
219
+ let mut skip_zero_row = false;
220
+ if source_variable.is_some()
221
+ && destination_variable.is_some()
222
+ && source_variable == destination_variable
223
+ {
224
+ // Extract the source node binding
225
+ let source_binding = match zero_row.get(source_variable.unwrap())
226
+ {
227
+ Ok(value::Value::Node(n)) => Some(n),
228
+ _ => None,
229
+ };
230
+ // Extract the destination node binding (should be start_node for zero-rep case)
231
+ let dest_binding = match zero_row.get(destination_variable.unwrap())
232
+ {
233
+ Ok(value::Value::Node(n)) => Some(n),
234
+ _ => None,
235
+ };
236
+ // For zero-rep case, both should point to start_node, so they should always be equal
237
+ // This check is mostly defensive; the zero-rep case should always satisfy this.
238
+ if let (Some(src), Some(dst)) = (source_binding, dest_binding)
239
+ && src.key() != dst.key()
240
+ {
241
+ skip_zero_row = true;
242
+ }
243
+ }
244
+
245
+ if !skip_zero_row
246
+ && path::check_destination_filter(destination_filter, &zero_row, parameters)?
247
+ {
248
+ new_rows.push(zero_row);
249
+ found_paths += 1;
250
+ }
251
+ }
252
+
253
+ // BFS main loop
254
+ while !frontier.is_empty() && (is_groups_mode || found_paths < k)
255
+ {
256
+ let frontier_node = match frontier.pop_front()
257
+ {
258
+ Some(fn_) => fn_,
259
+ None => break,
260
+ };
261
+
262
+ // Track depth changes for SHORTEST GROUPS. This must finalize the
263
+ // PREVIOUS depth's completion status before this node's own emission
264
+ // runs - otherwise a row emitted by the first node of a new depth gets
265
+ // misattributed as evidence the previous depth admitted rows, since
266
+ // current_depth_admitted_rows would be set by this same node before
267
+ // the transition check ever reads it.
268
+ if frontier_node.depth > processing_depth
269
+ {
270
+ processing_depth = frontier_node.depth;
271
+ }
272
+
273
+ if is_groups_mode && processing_depth > current_depth
274
+ {
275
+ if current_depth_admitted_rows
276
+ {
277
+ depths_completed += 1;
278
+ }
279
+ current_depth = processing_depth;
280
+ current_depth_admitted_rows = false;
281
+
282
+ if depths_completed >= k
283
+ {
284
+ break;
285
+ }
286
+ }
287
+
288
+ // Check if this node reaches destination and should be emitted
289
+ if frontier_node.depth >= min_hops
290
+ {
291
+ let mut emit_row = row.clone();
292
+
293
+ // Bind source node
294
+ if let Some(sv) = source_variable
295
+ {
296
+ emit_row.set(*sv, start_node.clone().into())?;
297
+ }
298
+
299
+ // Bind group variables as arrays, except for source/destination which stay as scalars
300
+ for (var_idx, var_col) in group_variables.iter().enumerate()
301
+ {
302
+ // Skip wrapping source and destination columns - they're already bound
303
+ // as scalars and should not be overwritten as arrays
304
+ if Some(*var_col) == *source_variable || Some(*var_col) == *destination_variable
305
+ {
306
+ continue;
307
+ }
308
+
309
+ emit_row.set(
310
+ *var_col,
311
+ value::Value::Array(frontier_node.group_lists[var_idx].clone()),
312
+ )?;
313
+ }
314
+
315
+ // Bind destination and path using shared function (fixes first-node-found heuristic in bfs.rs)
316
+ let (terminal_node, path) = path::bind_destination_and_path(
317
+ &frontier_node.row,
318
+ *group_terminal_variable,
319
+ group_variables,
320
+ &frontier_node.path_edges,
321
+ &start_node,
322
+ );
323
+
324
+ // Bind destination node (using pre-computed terminal_node)
325
+ if let Some(dv) = destination_variable
326
+ && let Some(ref node) = terminal_node
327
+ {
328
+ emit_row.set(*dv, node.clone().into())?;
329
+ }
330
+
331
+ // Save the terminal_node's key BEFORE it gets moved
332
+ let terminal_node_key = terminal_node.as_ref().map(|n| n.key());
333
+
334
+ // Bind path variable (using pre-computed path)
335
+ if let Some(pv) = path_variable
336
+ && let Some(path_value) = path
337
+ {
338
+ emit_row.set(*pv, path_value.into())?;
339
+ }
340
+
341
+ // Apply path_filter if present
342
+ let passes_path_filter = if let Some(pf) = path_filter
343
+ {
344
+ if pf.is_empty()
345
+ {
346
+ true
347
+ }
348
+ else
349
+ {
350
+ let mut eval_stack = Stack::default();
351
+ eval_instructions(&mut eval_stack, &emit_row, pf, parameters)?;
352
+ eval_stack.try_pop_as_boolean()?
353
+ }
354
+ }
355
+ else
356
+ {
357
+ true
358
+ };
359
+
360
+ // Apply destination_filter using shared function
361
+ let passes_destination_filter =
362
+ path::check_destination_filter(destination_filter, &emit_row, parameters)?;
363
+
364
+ // When destination_filter is empty (meaning the destination variable was already set),
365
+ // and the source and destination variables are the same, check that the destination node
366
+ // equals the start node (for patterns like "(a) ... (a)").
367
+ let is_same_variable_different_nodes = if destination_filter.is_empty()
368
+ && source_variable.is_some()
369
+ && source_variable == destination_variable
370
+ {
371
+ // The destination (terminal_node) must equal the start node
372
+ if let Some(tnk) = terminal_node_key
373
+ {
374
+ tnk != start_node.key()
375
+ }
376
+ else
377
+ {
378
+ false
379
+ }
380
+ }
381
+ else
382
+ {
383
+ false
384
+ };
385
+
386
+ if passes_path_filter && passes_destination_filter && !is_same_variable_different_nodes
387
+ {
388
+ new_rows.push(emit_row);
389
+ found_paths += 1;
390
+ // Track that this depth produced at least one admitted row (WI#693)
391
+ if frontier_node.depth == processing_depth
392
+ {
393
+ current_depth_admitted_rows = true;
394
+ }
395
+
396
+ // For SHORTEST mode, stop immediately when k is reached
397
+ if !is_groups_mode && found_paths >= k
398
+ {
399
+ break;
400
+ }
401
+ }
402
+ }
403
+
404
+ // Expansion is independent of both emission and min_hops: a node
405
+ // below min_hops still needs to search further, since a longer path
406
+ // through it might satisfy the minimum even though it doesn't itself.
407
+ if frontier_node.depth < max_hops
408
+ {
409
+ // Repetition N's landing point becomes repetition N+1's starting point.
410
+ // Nothing does this automatically - group_entry_variable and
411
+ // group_terminal_variable are different columns.
412
+ let mut next_row = frontier_node.row.clone();
413
+ let terminal_value = next_row.get(*group_terminal_variable)?.clone();
414
+ next_row.set(*group_entry_variable, terminal_value)?;
415
+
416
+ // Execute group_instructions to get next candidates
417
+ let next_candidates = crate::interpreter::evaluators::execute_block_matches(
418
+ group_instructions,
419
+ store,
420
+ tx,
421
+ graph_name,
422
+ stack,
423
+ parameters,
424
+ vec![next_row],
425
+ allow_recursive,
426
+ )?;
427
+
428
+ // Add next candidates to frontier
429
+ for next_row in next_candidates
430
+ {
431
+ // Extract edges using shared function
432
+ let candidate_edges = path::extract_edges(&next_row, group_variables);
433
+
434
+ // Check uniqueness
435
+ let all_available = candidate_edges
436
+ .iter()
437
+ .all(|ek| uniqueness.is_reusable(&frontier_node.used_edges, ek));
438
+
439
+ if !all_available
440
+ {
441
+ continue;
442
+ }
443
+
444
+ // Check group_where_filter
445
+ if let Some(gwf) = group_where_filter
446
+ && !gwf.is_empty()
447
+ {
448
+ let mut eval_stack = Stack::default();
449
+ eval_instructions(&mut eval_stack, &next_row, gwf, parameters)?;
450
+ let passes: bool = eval_stack.try_pop_as_boolean()?;
451
+ if !passes
452
+ {
453
+ continue;
454
+ }
455
+ }
456
+
457
+ // Create new frontier node
458
+ let mut new_node = FrontierNode {
459
+ row: next_row,
460
+ depth: frontier_node.depth + 1,
461
+ path_edges: frontier_node.path_edges.clone(),
462
+ group_lists: frontier_node.group_lists.to_vec(),
463
+ used_edges: frontier_node.used_edges.clone(),
464
+ };
465
+
466
+ // Mark edges as used
467
+ for edge_key in &candidate_edges
468
+ {
469
+ uniqueness.mark_used(&mut new_node.used_edges, *edge_key);
470
+ }
471
+
472
+ // Push group variable values
473
+ for (var_idx, var_col) in group_variables.iter().enumerate()
474
+ {
475
+ if let Ok(val) = new_node.row.get(*var_col)
476
+ {
477
+ new_node.group_lists[var_idx].push(val.clone());
478
+
479
+ // Add edge to path_edges
480
+ if let value::Value::Edge(edge) = val
481
+ {
482
+ new_node.path_edges.push(edge.clone());
483
+ }
484
+ }
485
+ }
486
+
487
+ frontier.push_back(new_node);
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ Ok(())
494
+ }