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,267 @@
1
+ use rustc_hash::FxHashSet;
2
+
3
+ use crate::{
4
+ graph,
5
+ interpreter::{
6
+ evaluators::{Stack, TryPopInto, eval_instructions},
7
+ instructions,
8
+ path::PathUniqueness,
9
+ },
10
+ prelude::*,
11
+ store,
12
+ value_table::{self, MutableRowInterface, RowInterface},
13
+ };
14
+
15
+ #[allow(clippy::too_many_arguments)]
16
+ pub(crate) fn execute<TStore: store::Store>(
17
+ recursive_range: &std::ops::Range<usize>,
18
+ query: &store::SelectEdgeQuery,
19
+ left_variable: &Option<value_table::ColId>,
20
+ edge_variable: &Option<value_table::ColId>,
21
+ right_variable: &Option<value_table::ColId>,
22
+ path_variable: &Option<value_table::ColId>,
23
+ filter: &instructions::Instructions,
24
+ directivity: graphcore::EdgeDirectivity,
25
+ store: &TStore,
26
+ tx: &mut TStore::TransactionBox,
27
+ graph_name: &str,
28
+ parameters: &crate::value::ValueMap,
29
+ row: &value_table::Row,
30
+ allow_recursive: bool,
31
+ new_rows: &mut Vec<value_table::Row>,
32
+ uniqueness: PathUniqueness,
33
+ ) -> Result<()>
34
+ {
35
+ if !allow_recursive
36
+ {
37
+ return Err(InternalError::UnsupportedRecursivePatternInMerge.into());
38
+ }
39
+
40
+ // Recursive DFS
41
+ let min_hops = recursive_range.start;
42
+ let max_hops = recursive_range.end;
43
+ let capacity = max_hops.min(100);
44
+
45
+ let bound_dest_key: Option<graph::Key> = if let Some(right_col) = right_variable
46
+ {
47
+ match row.get(*right_col)?
48
+ {
49
+ value::Value::Node(node) => Some(node.key()),
50
+ _ => None,
51
+ }
52
+ }
53
+ else
54
+ {
55
+ None
56
+ };
57
+
58
+ let start_nodes: Vec<graph::Node> = {
59
+ let source_query = query.source_query();
60
+ store.select_nodes(tx, graph_name, source_query)?
61
+ };
62
+
63
+ struct Frame
64
+ {
65
+ node: graph::Node,
66
+ edges: Vec<store::EdgeResult>,
67
+ edge_index: usize,
68
+ depth: usize,
69
+ }
70
+
71
+ for start_node in start_nodes
72
+ {
73
+ let mut path_edges: Vec<graph::Edge> = Vec::with_capacity(capacity);
74
+ let mut used_edges: FxHashSet<graph::Key> =
75
+ FxHashSet::with_capacity_and_hasher(capacity, Default::default());
76
+ let mut dedup_set: FxHashSet<(Option<graph::Key>, Option<graph::Key>, usize)> =
77
+ FxHashSet::with_capacity_and_hasher(capacity, Default::default());
78
+
79
+ let initial_edges = {
80
+ let q =
81
+ store::SelectEdgeQuery::select_source_keys(store::SelectNodeQuery::select_keys(vec![
82
+ start_node.key(),
83
+ ]));
84
+ store.select_edges(tx, graph_name, q, directivity)?
85
+ };
86
+
87
+ let mut dfs_stack: Vec<Frame> = vec![Frame {
88
+ node: start_node.clone(),
89
+ edges: initial_edges,
90
+ edge_index: 0,
91
+ depth: 0,
92
+ }];
93
+
94
+ while !dfs_stack.is_empty()
95
+ {
96
+ let mut push_next: Option<Frame> = None;
97
+ let mut should_pop = false;
98
+
99
+ let dfs_intermediate_nodes: Vec<graph::Node> = if dfs_stack.len() >= 2
100
+ {
101
+ dfs_stack[1..dfs_stack.len() - 1]
102
+ .iter()
103
+ .map(|f| f.node.clone())
104
+ .collect()
105
+ }
106
+ else
107
+ {
108
+ Vec::new()
109
+ };
110
+
111
+ {
112
+ let frame = dfs_stack.last_mut().unwrap();
113
+
114
+ // Emit row
115
+ if frame.edge_index == 0 && frame.depth >= min_hops
116
+ {
117
+ let current_node = &frame.node;
118
+
119
+ let mut new_row = row.clone();
120
+
121
+ if let Some(left_var) = left_variable
122
+ {
123
+ new_row.set(*left_var, start_node.clone().into())?;
124
+ }
125
+ if let Some(right_var) = right_variable
126
+ {
127
+ new_row.set(*right_var, current_node.clone().into())?;
128
+ }
129
+ if let Some(edge_var) = edge_variable
130
+ && let Some(last_edge) = path_edges.last()
131
+ {
132
+ new_row.set(*edge_var, last_edge.clone().into())?;
133
+ }
134
+ if let Some(path_var) = path_variable
135
+ {
136
+ let path = graph::Path::new(
137
+ start_node.clone(),
138
+ path_edges
139
+ .clone()
140
+ .into_iter()
141
+ .zip(
142
+ dfs_intermediate_nodes
143
+ .clone()
144
+ .into_iter()
145
+ .chain(std::iter::once(current_node.clone())),
146
+ )
147
+ .collect(),
148
+ );
149
+ new_row.set(*path_var, path.into())?;
150
+ }
151
+
152
+ let passes_filter = if filter.is_empty()
153
+ {
154
+ true
155
+ }
156
+ else
157
+ {
158
+ let mut eval_stack = Stack::default();
159
+ eval_stack.push(true);
160
+ if let Some(last_edge) = path_edges.last()
161
+ {
162
+ eval_stack.push(last_edge.clone());
163
+ }
164
+ eval_instructions(&mut eval_stack, &new_row, filter, parameters)?;
165
+ eval_stack.try_pop()?;
166
+ eval_stack.try_pop_into()?
167
+ };
168
+
169
+ let matches_dest = if let Some(dest_key) = bound_dest_key
170
+ {
171
+ current_node.key() == dest_key
172
+ }
173
+ else
174
+ {
175
+ true
176
+ };
177
+
178
+ if passes_filter && matches_dest
179
+ {
180
+ let left_key_opt: Option<graph::Key> = if left_variable.is_some()
181
+ {
182
+ Some(start_node.key())
183
+ }
184
+ else
185
+ {
186
+ None
187
+ };
188
+ let right_key_opt: Option<graph::Key> = Some(current_node.key());
189
+ let path_len = path_edges.len();
190
+ let sig = (left_key_opt, right_key_opt, path_len);
191
+
192
+ if !dedup_set.contains(&sig)
193
+ {
194
+ dedup_set.insert(sig);
195
+ new_rows.push(new_row);
196
+ }
197
+ }
198
+ }
199
+
200
+ // Stop for max depth
201
+ if frame.depth == max_hops || frame.edge_index >= frame.edges.len()
202
+ {
203
+ should_pop = true;
204
+ }
205
+ else
206
+ {
207
+ // Next hop
208
+ let edge_result = &frame.edges[frame.edge_index];
209
+ frame.edge_index += 1;
210
+
211
+ let edge = edge_result.path.to_edge();
212
+ let edge_key = edge.key();
213
+
214
+ if !uniqueness.is_reusable(&used_edges, &edge_key)
215
+ {
216
+ continue;
217
+ }
218
+
219
+ let next_node = if edge_result.reversed
220
+ {
221
+ edge_result.path.source().clone()
222
+ }
223
+ else
224
+ {
225
+ edge_result.path.destination().clone()
226
+ };
227
+
228
+ uniqueness.mark_used(&mut used_edges, edge_key);
229
+ path_edges.push(edge.clone());
230
+
231
+ let next_edges = {
232
+ let q = store::SelectEdgeQuery::select_source_keys(
233
+ store::SelectNodeQuery::select_keys(vec![next_node.key()]),
234
+ );
235
+ store.select_edges(tx, graph_name, q, directivity)?
236
+ };
237
+
238
+ push_next = Some(Frame {
239
+ node: next_node.clone(),
240
+ edges: next_edges,
241
+ edge_index: 0,
242
+ depth: frame.depth + 1,
243
+ });
244
+ }
245
+ }
246
+
247
+ // Mutate stack
248
+ if should_pop
249
+ {
250
+ dfs_stack.pop();
251
+ if let Some(last_edge) = path_edges.pop()
252
+ {
253
+ uniqueness.mark_unused(&mut used_edges, &last_edge.key());
254
+ }
255
+ continue;
256
+ }
257
+
258
+ if let Some(frame) = push_next
259
+ {
260
+ dfs_stack.push(frame);
261
+ continue;
262
+ }
263
+ }
264
+ }
265
+
266
+ Ok(())
267
+ }
@@ -0,0 +1,418 @@
1
+ use rustc_hash::FxHashSet;
2
+
3
+ use crate::{
4
+ graph,
5
+ interpreter::{
6
+ evaluators::{Stack, eval_instructions},
7
+ instructions,
8
+ path::{self, PathUniqueness},
9
+ },
10
+ prelude::*,
11
+ store,
12
+ value_table::{self, MutableRowInterface, RowInterface},
13
+ };
14
+
15
+ struct Frame
16
+ {
17
+ candidates: Vec<value_table::Row>,
18
+ candidate_index: usize,
19
+ depth: usize,
20
+ /// Edges marked as used when this frame was pushed.
21
+ /// Used during backtracking to unmark exactly the edges this frame consumed (WI#692).
22
+ edges_marked: Vec<graph::Key>,
23
+ }
24
+
25
+ /// Execute a quantified path pattern using list-capture (group repetition) strategy.
26
+ ///
27
+ /// This generalizes the DFS traversal: instead of each frame representing "one next edge,"
28
+ /// each frame represents "one candidate result from executing the group's group_instructions."
29
+ /// Group variables are accumulated into lists, pushed/popped with the frame stack.
30
+ ///
31
+ /// Design decisions (WI#672):
32
+ /// 1. Edge detection: Extract edges at runtime by checking value types (Value::Edge)
33
+ /// 2. group_where_filter: Evaluated before acceptance, pruning as traversal progresses
34
+ /// 3. path_variable: Accumulated from edges in group variables (explicit edges only)
35
+ #[allow(clippy::too_many_arguments)]
36
+ pub(crate) fn execute<TStore: store::Store>(
37
+ block: &instructions::BlockMatch,
38
+ store: &TStore,
39
+ tx: &mut TStore::TransactionBox,
40
+ graph_name: &str,
41
+ stack: &mut Stack,
42
+ parameters: &crate::value::ValueMap,
43
+ row: &value_table::Row,
44
+ allow_recursive: bool,
45
+ new_rows: &mut Vec<value_table::Row>,
46
+ ) -> Result<()>
47
+ {
48
+ let instructions::BlockMatch::MatchQuantifiedPath {
49
+ source_instructions,
50
+ source_variable,
51
+ source_filter,
52
+ destination_variable,
53
+ destination_filter,
54
+ path_variable,
55
+ quantifier,
56
+ group_instructions,
57
+ group_variables,
58
+ group_terminal_variable,
59
+ group_entry_variable,
60
+ group_where_filter,
61
+ path_filter: _,
62
+ strategy: _,
63
+ } = block
64
+ else
65
+ {
66
+ unreachable!("execute called with a non-MatchQuantifiedPath block");
67
+ };
68
+
69
+ if !allow_recursive
70
+ {
71
+ return Err(InternalError::UnsupportedRecursivePatternInMerge.into());
72
+ }
73
+
74
+ let min_hops = quantifier.start;
75
+ let max_hops = quantifier.end;
76
+ let capacity = max_hops.min(100);
77
+
78
+ // Resolve start nodes via shared function
79
+ let start_nodes = path::resolve_start_nodes(
80
+ source_instructions,
81
+ source_variable,
82
+ source_filter,
83
+ store,
84
+ tx,
85
+ graph_name,
86
+ parameters,
87
+ row,
88
+ )?;
89
+
90
+ let uniqueness = PathUniqueness::Trail;
91
+
92
+ for start_node in start_nodes
93
+ {
94
+ let mut used_edges: FxHashSet<graph::Key> =
95
+ FxHashSet::with_capacity_and_hasher(capacity, Default::default());
96
+ let mut path_edges: Vec<graph::Edge> = Vec::with_capacity(capacity);
97
+ let mut group_lists: Vec<Vec<value::Value>> =
98
+ vec![Vec::with_capacity(capacity); group_variables.len()];
99
+
100
+ // Zero-repetition case: if min_hops == 0, the start node itself,
101
+ // with no edges traversed, is a valid match - nothing else in this
102
+ // function ever considers this, since initial_candidates always
103
+ // represents one repetition already completed.
104
+ if min_hops == 0
105
+ {
106
+ let mut zero_row = row.clone();
107
+ if let Some(sv) = source_variable
108
+ {
109
+ zero_row.set(*sv, start_node.clone().into())?;
110
+ }
111
+ for var_col in group_variables
112
+ {
113
+ zero_row.set(*var_col, value::Value::Array(Vec::new()))?;
114
+ }
115
+ if let Some(dv) = destination_variable
116
+ {
117
+ zero_row.set(*dv, start_node.clone().into())?;
118
+ }
119
+ if let Some(pv) = path_variable
120
+ {
121
+ zero_row.set(*pv, graph::Path::new(start_node.clone(), vec![]).into())?;
122
+ }
123
+ if path::check_destination_filter(destination_filter, &zero_row, parameters)?
124
+ {
125
+ new_rows.push(zero_row);
126
+ }
127
+ }
128
+
129
+ let mut position_row = row.clone();
130
+ if let Some(sv) = source_variable
131
+ {
132
+ position_row.set(*sv, start_node.clone().into())?;
133
+ }
134
+ // For the first iteration, set the group's entry point to the start node
135
+ // (compile_match_edge expects this column to be pre-populated with the source node)
136
+ position_row.set(*group_entry_variable, start_node.clone().into())?;
137
+
138
+ // Initial group execution from source node
139
+ let initial_candidates = crate::interpreter::evaluators::execute_block_matches(
140
+ group_instructions,
141
+ store,
142
+ tx,
143
+ graph_name,
144
+ stack,
145
+ parameters,
146
+ vec![position_row],
147
+ allow_recursive,
148
+ )?;
149
+
150
+ let mut dfs_stack: Vec<Frame> = vec![Frame {
151
+ candidates: initial_candidates.clone(),
152
+ candidate_index: 0,
153
+ // depth = number of real repetitions completed. initial_candidates
154
+ // is the result of ONE execution of group_instructions, so this is
155
+ // already 1 repetition done, not 0 - matching dfs.rs's convention
156
+ // where depth means "hops taken to reach this position."
157
+ depth: 1,
158
+ edges_marked: Vec::new(),
159
+ }];
160
+
161
+ while !dfs_stack.is_empty() && !initial_candidates.is_empty()
162
+ {
163
+ let mut push_next: Option<Frame> = None;
164
+ let mut should_pop = false;
165
+
166
+ {
167
+ let frame = dfs_stack.last_mut().unwrap();
168
+
169
+ // Emission: if depth >= min_hops and at start of candidate list, emit a row
170
+ if frame.candidate_index == 0 && frame.depth >= min_hops
171
+ {
172
+ // For emission, we need to include edges from the current frame's first candidate
173
+ // along with accumulated path_edges from previous frames.
174
+ let current_candidate = &frame.candidates[0];
175
+
176
+ // Check group_where_filter against the current candidate BEFORE emission.
177
+ // This prevents emitting candidates that don't satisfy the filter.
178
+ let should_emit = if let Some(gwf) = group_where_filter
179
+ && !gwf.is_empty()
180
+ {
181
+ let mut eval_stack = Stack::default();
182
+ eval_instructions(&mut eval_stack, current_candidate, gwf, parameters)?;
183
+ let passes: bool = eval_stack.try_pop_as_boolean()?;
184
+ passes
185
+ }
186
+ else
187
+ {
188
+ true
189
+ };
190
+
191
+ if should_emit
192
+ {
193
+ let mut new_row = row.clone();
194
+
195
+ // Save the start node's key - this is what the source should be bound to
196
+ let start_node_key = start_node.key();
197
+
198
+ // Bind source node
199
+ if let Some(sv) = source_variable
200
+ {
201
+ new_row.set(*sv, start_node.clone().into())?;
202
+ }
203
+
204
+ let mut emission_path_edges = path_edges.clone();
205
+ let mut emission_group_lists = group_lists.clone();
206
+
207
+ // Extract edges from current frame's first candidate
208
+ for (var_idx, var_col) in group_variables.iter().enumerate()
209
+ {
210
+ if let Ok(val) = current_candidate.get(*var_col)
211
+ {
212
+ emission_group_lists[var_idx].push(val.clone());
213
+ if let value::Value::Edge(edge) = val
214
+ {
215
+ emission_path_edges.push(edge.clone());
216
+ }
217
+ }
218
+ }
219
+
220
+ // BEFORE overwriting group_variables with arrays, extract and bind destination/path
221
+ // using the shared function to ensure consistent logic across strategies
222
+ let (terminal_node, path) = path::bind_destination_and_path(
223
+ current_candidate,
224
+ *group_terminal_variable,
225
+ group_variables,
226
+ &emission_path_edges,
227
+ &start_node,
228
+ );
229
+
230
+ // NOW overwrite group variables with their accumulated arrays,
231
+ // except for source/destination which stay as scalars
232
+ for (var_idx, var_col) in group_variables.iter().enumerate()
233
+ {
234
+ // Skip wrapping source and destination columns - they're already bound
235
+ // as scalars and should not be overwritten as arrays
236
+ if Some(*var_col) == *source_variable || Some(*var_col) == *destination_variable
237
+ {
238
+ continue;
239
+ }
240
+
241
+ new_row.set(
242
+ *var_col,
243
+ value::Value::Array(emission_group_lists[var_idx].clone()),
244
+ )?;
245
+ }
246
+
247
+ // Save the terminal_node's key BEFORE it gets moved
248
+ let terminal_node_key = terminal_node.as_ref().map(|n| n.key());
249
+
250
+ // Bind destination node (using pre-computed terminal_node)
251
+ if let Some(dv) = destination_variable
252
+ && let Some(node) = terminal_node
253
+ {
254
+ new_row.set(*dv, node.into())?;
255
+ }
256
+
257
+ // Bind path variable (using pre-computed path)
258
+ if let Some(pv) = path_variable
259
+ && let Some(path_value) = path
260
+ {
261
+ new_row.set(*pv, path_value.into())?;
262
+ }
263
+
264
+ // Apply destination_filter before emission using shared function
265
+ let passes_destination_filter =
266
+ path::check_destination_filter(destination_filter, &new_row, parameters)?;
267
+
268
+ // When destination_filter is empty (meaning the destination variable was already set),
269
+ // and the source and destination variables are the same column, check that the
270
+ // destination node equals the start node (for patterns like "(a) ... (a)").
271
+ let is_same_variable_different_nodes = if destination_filter.is_empty()
272
+ && source_variable.is_some()
273
+ && source_variable == destination_variable
274
+ {
275
+ // The destination (terminal_node) must equal the start node
276
+ if let Some(tnk) = terminal_node_key
277
+ {
278
+ tnk != start_node_key
279
+ }
280
+ else
281
+ {
282
+ false
283
+ }
284
+ }
285
+ else
286
+ {
287
+ false
288
+ };
289
+
290
+ if passes_destination_filter && !is_same_variable_different_nodes
291
+ {
292
+ new_rows.push(new_row);
293
+ }
294
+ }
295
+ }
296
+
297
+ // Stop condition
298
+ if frame.depth == max_hops || frame.candidate_index >= frame.candidates.len()
299
+ {
300
+ should_pop = true;
301
+ }
302
+ else
303
+ {
304
+ // Advance: process next candidate
305
+ let mut candidate_row = frame.candidates[frame.candidate_index].clone();
306
+ frame.candidate_index += 1;
307
+
308
+ // Extract edges from this candidate using shared function
309
+ let candidate_edges = path::extract_edges(&candidate_row, group_variables);
310
+
311
+ // Check uniqueness: all edges must be available
312
+ let all_available = candidate_edges
313
+ .iter()
314
+ .all(|ek| uniqueness.is_reusable(&used_edges, ek));
315
+
316
+ if !all_available
317
+ {
318
+ continue;
319
+ }
320
+
321
+ // Check group_where_filter if present (pruning before acceptance)
322
+ if let Some(gwf) = group_where_filter
323
+ && !gwf.is_empty()
324
+ {
325
+ let mut eval_stack = Stack::default();
326
+ eval_instructions(&mut eval_stack, &candidate_row, gwf, parameters)?;
327
+ let passes: bool = eval_stack.try_pop_as_boolean()?;
328
+ if !passes
329
+ {
330
+ continue;
331
+ }
332
+ }
333
+
334
+ // Mark edges as used and push values onto lists
335
+ for edge_key in &candidate_edges
336
+ {
337
+ uniqueness.mark_used(&mut used_edges, *edge_key);
338
+ }
339
+
340
+ for (var_idx, var_col) in group_variables.iter().enumerate()
341
+ {
342
+ if let Ok(val) = candidate_row.get(*var_col)
343
+ {
344
+ group_lists[var_idx].push(val.clone());
345
+
346
+ // If it's an edge, add to path_edges
347
+ if let value::Value::Edge(edge) = val
348
+ {
349
+ path_edges.push(edge.clone());
350
+ }
351
+ }
352
+ }
353
+
354
+ // Repetition N's landing point becomes repetition N+1's starting point.
355
+ // Nothing does this automatically - group_entry_variable and
356
+ // group_terminal_variable are different columns.
357
+ let terminal_value = candidate_row.get(*group_terminal_variable)?.clone();
358
+ candidate_row.set(*group_entry_variable, terminal_value)?;
359
+
360
+ // Execute group_instructions for next frame
361
+ let next_candidates = crate::interpreter::evaluators::execute_block_matches(
362
+ group_instructions,
363
+ store,
364
+ tx,
365
+ graph_name,
366
+ stack,
367
+ parameters,
368
+ vec![candidate_row],
369
+ allow_recursive,
370
+ )?;
371
+
372
+ if !next_candidates.is_empty()
373
+ {
374
+ push_next = Some(Frame {
375
+ candidates: next_candidates,
376
+ candidate_index: 0,
377
+ depth: frame.depth + 1,
378
+ edges_marked: candidate_edges.clone(),
379
+ });
380
+ }
381
+ }
382
+ }
383
+
384
+ // Backtrack
385
+ if should_pop
386
+ {
387
+ // Get the current frame before popping to access edges_marked
388
+ if let Some(current_frame) = dfs_stack.pop()
389
+ {
390
+ // Pop group variables and path edges for each edge marked by this frame (WI#692)
391
+ for list in &mut group_lists
392
+ {
393
+ list.pop();
394
+ }
395
+
396
+ // Unmark all edges that this frame marked (in reverse order for path_edges)
397
+ for edge_key in current_frame.edges_marked.iter().rev()
398
+ {
399
+ if path_edges.pop().is_some()
400
+ {
401
+ uniqueness.mark_unused(&mut used_edges, edge_key);
402
+ }
403
+ }
404
+ }
405
+ continue;
406
+ }
407
+
408
+ // Push next frame
409
+ if let Some(frame) = push_next
410
+ {
411
+ dfs_stack.push(frame);
412
+ continue;
413
+ }
414
+ }
415
+ }
416
+
417
+ Ok(())
418
+ }