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
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
use crate::{prelude::*, value_table::ColId};
|
|
2
2
|
|
|
3
|
+
#[derive(Debug)]
|
|
4
|
+
pub(crate) enum StopCondition
|
|
5
|
+
{
|
|
6
|
+
Shortest(Instructions),
|
|
7
|
+
ShortestGroups(Instructions),
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#[derive(Debug)]
|
|
11
|
+
pub(crate) enum PathStrategy
|
|
12
|
+
{
|
|
13
|
+
Dfs,
|
|
14
|
+
Bfs
|
|
15
|
+
{
|
|
16
|
+
stop_condition: StopCondition,
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
#[derive(Debug)]
|
|
4
21
|
#[allow(clippy::large_enum_variant)]
|
|
5
22
|
pub(crate) enum Instruction
|
|
@@ -79,6 +96,9 @@ pub(crate) enum Instruction
|
|
|
79
96
|
DivisionBinaryOperator,
|
|
80
97
|
ModuloBinaryOperator,
|
|
81
98
|
ExponentBinaryOperator,
|
|
99
|
+
StringContainsOperator,
|
|
100
|
+
StringStartsWithOperator,
|
|
101
|
+
StringEndsWithOperator,
|
|
82
102
|
}
|
|
83
103
|
|
|
84
104
|
pub(crate) type Instructions = Vec<Instruction>;
|
|
@@ -100,6 +120,7 @@ pub(crate) struct CreateAction
|
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
#[derive(Debug)]
|
|
123
|
+
#[allow(clippy::enum_variant_names)]
|
|
103
124
|
pub(crate) enum BlockMatch
|
|
104
125
|
{
|
|
105
126
|
MatchNode
|
|
@@ -119,6 +140,60 @@ pub(crate) enum BlockMatch
|
|
|
119
140
|
directivity: graphcore::EdgeDirectivity,
|
|
120
141
|
recursive_range: Option<std::ops::Range<usize>>,
|
|
121
142
|
},
|
|
143
|
+
MatchQuantifiedPath
|
|
144
|
+
{
|
|
145
|
+
/// Compiled query for the group's fixed anchor before it (`qp.source`).
|
|
146
|
+
/// Pops one `SelectNodeQuery`, same as an ordinary `MatchNode`'s
|
|
147
|
+
/// `instructions` field — reuses `compile_match_node` directly (WI2).
|
|
148
|
+
source_instructions: Instructions,
|
|
149
|
+
source_variable: Option<ColId>,
|
|
150
|
+
source_filter: Instructions,
|
|
151
|
+
/// The destination node is validated only at emission time via
|
|
152
|
+
/// `destination_filter`, not queried from the store.
|
|
153
|
+
destination_variable: Option<ColId>,
|
|
154
|
+
destination_filter: Instructions,
|
|
155
|
+
/// The `p = ...` binding, if present.
|
|
156
|
+
path_variable: Option<ColId>,
|
|
157
|
+
/// Resolved `min..max` from `qp.quantifier` — always `Some` in
|
|
158
|
+
/// practice by the time this instruction exists (WI3 only ever
|
|
159
|
+
/// constructs one from a `QuantifiedPathPattern` with `group.len() > 1`,
|
|
160
|
+
/// and Milestone 2/4's `None`-quantifier scoping-wrapper use case,
|
|
161
|
+
/// design §1d, isn't reachable from real query text until Milestone 5).
|
|
162
|
+
quantifier: std::ops::Range<usize>,
|
|
163
|
+
/// The group's inner pattern sequence, compiled the same way an
|
|
164
|
+
/// ordinary top-level pattern chain is (WI2) — run once per repetition.
|
|
165
|
+
group_instructions: Vec<BlockMatch>,
|
|
166
|
+
/// Every variable declared inside the group (`n`, `r`, `m` for
|
|
167
|
+
/// `(a) ((n)-[r]->(m)){1,5} (b)`) — however many there are. Bound as a
|
|
168
|
+
/// list at execution (Epic 8); this field only declares *which*
|
|
169
|
+
/// columns need that treatment.
|
|
170
|
+
group_variables: Vec<ColId>,
|
|
171
|
+
/// Which entry in `group_variables` is the group's own terminal node
|
|
172
|
+
/// (the exit node of its last inner edge) - guaranteed present by the
|
|
173
|
+
/// parser-level fix. Used for chaining repetitions and for
|
|
174
|
+
/// checking `destination_filter`/binding `destination_variable`,
|
|
175
|
+
/// replacing the "first Node found" heuristic.
|
|
176
|
+
group_terminal_variable: ColId,
|
|
177
|
+
/// Which entry in `group_variables` is the group's own entry node
|
|
178
|
+
/// (the source node of its first inner edge) - guaranteed present by the
|
|
179
|
+
/// parser-level fix. Used to wire the previous repetition's terminal
|
|
180
|
+
/// to the next repetition's entry, so pattern search is constrained
|
|
181
|
+
/// from the current position rather than all nodes.
|
|
182
|
+
group_entry_variable: ColId,
|
|
183
|
+
/// Compiled group `WHERE`, if present (design §1, `qp.group_where`) —
|
|
184
|
+
/// evaluated once per repetition once Epic 8 wires up execution.
|
|
185
|
+
/// Genuine groups only; never `Some` on wrapper-promoted patterns.
|
|
186
|
+
group_where_filter: Option<Instructions>,
|
|
187
|
+
/// Whole-completed-path predicate, from the selector-scoped wrapper
|
|
188
|
+
/// (`qp.path_where`). Evaluated once a candidate reaches `destination`,
|
|
189
|
+
/// before it's admitted to shortest-path selection. Distinct from
|
|
190
|
+
/// `group_where_filter`'s per-repetition timing; never both `Some` on
|
|
191
|
+
/// the same instruction in practice (a wrapper-promoted pattern has no
|
|
192
|
+
/// genuine group `WHERE` of its own).
|
|
193
|
+
path_filter: Option<Instructions>,
|
|
194
|
+
/// The path strategy (Dfs or Bfs with stop condition) from the planner.
|
|
195
|
+
strategy: PathStrategy,
|
|
196
|
+
},
|
|
122
197
|
}
|
|
123
198
|
|
|
124
199
|
#[derive(Debug)]
|
|
@@ -127,6 +202,7 @@ pub(crate) struct RWAggregation
|
|
|
127
202
|
pub(crate) init_instructions: Instructions,
|
|
128
203
|
pub(crate) argument_instructions: Instructions,
|
|
129
204
|
pub(crate) aggregator: aggregators::Aggregator,
|
|
205
|
+
pub(crate) distinct: bool,
|
|
130
206
|
}
|
|
131
207
|
|
|
132
208
|
/// R(eturn)W(ith)Expression are expressions computed in Return or With blocks.
|
|
@@ -189,6 +265,7 @@ pub(crate) struct OrderBy
|
|
|
189
265
|
#[derive(Debug)]
|
|
190
266
|
pub(crate) struct Modifiers
|
|
191
267
|
{
|
|
268
|
+
pub distinct: bool,
|
|
192
269
|
pub limit: Option<Instructions>,
|
|
193
270
|
pub skip: Option<Instructions>,
|
|
194
271
|
pub order_by: Vec<OrderBy>,
|
|
@@ -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
|
+
}
|