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.
- checksums.yaml +4 -4
- data/ext/Cargo.toml +8 -8
- data/ext/db-index/Cargo.toml +2 -2
- data/ext/gqlitedb/Cargo.toml +0 -2
- data/ext/gqlitedb/src/aggregators.rs +28 -0
- data/ext/gqlitedb/src/compiler/expression_analyser.rs +33 -0
- data/ext/gqlitedb/src/compiler/variables_manager.rs +111 -11
- data/ext/gqlitedb/src/compiler.rs +506 -48
- data/ext/gqlitedb/src/connection.rs +14 -2
- data/ext/gqlitedb/src/consts.rs +1 -1
- data/ext/gqlitedb/src/error.rs +31 -0
- data/ext/gqlitedb/src/interpreter/evaluators.rs +153 -331
- data/ext/gqlitedb/src/interpreter/instructions.rs +77 -0
- data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
- data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
- data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
- data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
- data/ext/gqlitedb/src/interpreter/path.rs +542 -0
- data/ext/gqlitedb/src/planner.rs +231 -6
- data/ext/gqlitedb/src/query_result.rs +19 -4
- data/ext/gqlitedb/src/store/redb.rs +1 -1
- data/ext/gqlitedb/src/tests/compiler.rs +2454 -1
- data/ext/gqlitedb/src/tests/connection/postgres.rs +22 -0
- data/ext/gqlitedb/src/tests/connection/redb.rs +24 -0
- data/ext/gqlitedb/src/tests/connection/sqlite.rs +16 -0
- data/ext/gqlitedb/src/tests/connection.rs +101 -0
- data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
- data/ext/gqlitedb/src/tests/parser.rs +1225 -0
- data/ext/gqlitedb/src/tests/planner.rs +433 -2
- data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
- data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
- data/ext/gqlitedb/src/tests/templates/ast.rs +294 -1
- data/ext/gqlitedb/src/tests/templates/programs.rs +318 -0
- data/ext/gqliterb/Cargo.toml +0 -1
- data/ext/gqlparser/src/oc/ast.rs +72 -3
- data/ext/gqlparser/src/oc/error.rs +18 -0
- data/ext/gqlparser/src/oc/lexer.rs +157 -2
- data/ext/gqlparser/src/oc/parser/tests.rs +735 -29
- data/ext/gqlparser/src/oc/parser.rs +1312 -182
- data/ext/gqlparser/src/oc/string.rs +172 -0
- data/ext/gqlparser/src/oc.rs +1 -0
- data/ext/graphcore/src/value/value_map.rs +2 -2
- data/ext/graphcore/src/value.rs +13 -1
- metadata +6 -2
- 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
|
+
}
|