Skip to main content

lora_compiler/
logical.rs

1use lora_analyzer::symbols::VarId;
2use lora_analyzer::{
3    ResolvedClause, ResolvedExpr, ResolvedMergeAction, ResolvedPattern, ResolvedPatternPart,
4    ResolvedProjection, ResolvedRemoveItem, ResolvedSetItem, ResolvedSortItem,
5};
6use lora_ast::{Direction, RangeLiteral};
7
8pub type PlanNodeId = usize;
9
10#[derive(Debug, Clone)]
11pub struct LogicalPlan {
12    pub root: PlanNodeId,
13    pub nodes: Vec<LogicalOp>,
14}
15
16#[derive(Debug, Clone)]
17pub enum LogicalOp {
18    Argument(Argument),
19    NodeScan(NodeScan),
20    NodeByPropertyScan(NodeByPropertyScan),
21    NodeByPropertyRangeScan(NodeByPropertyRangeScan),
22    NodeByTextScan(NodeByTextScan),
23    NodeByPointScan(NodeByPointScan),
24    RelByPropertyRangeScan(RelByPropertyRangeScan),
25    RelByTextScan(RelByTextScan),
26    RelByPointScan(RelByPointScan),
27    Expand(Expand),
28    Filter(Filter),
29    Projection(Projection),
30    Unwind(Unwind),
31    Aggregation(Aggregation),
32    Sort(Sort),
33    Limit(Limit),
34    Merge(Merge),
35    Delete(Delete),
36    Set(Set),
37    Remove(Remove),
38    Create(Create),
39    Foreach(Foreach),
40    OptionalMatch(OptionalMatch),
41    PathBuild(PathBuild),
42    CallSubquery(CallSubquery),
43}
44
45/// `FOREACH (var IN list | body...)` — for each input row, evaluate
46/// the list, then run each body clause once per element with `var`
47/// bound to the element. The body is a flat list of resolved updating
48/// clauses applied for side effects only; the outer row is emitted
49/// unchanged after the loop.
50#[derive(Debug, Clone)]
51pub struct Foreach {
52    pub input: PlanNodeId,
53    pub variable: VarId,
54    pub list: ResolvedExpr,
55    pub body: Vec<ResolvedClause>,
56}
57
58/// `CALL { ... }` subquery: for each upstream row, runs the inner
59/// sub-plan with the upstream row as its initial argument, then
60/// emits the cartesian product of `(upstream row, inner row)` for
61/// each inner row produced. `new_vars` are the VarIds the inner
62/// RETURN exposes to the outer scope.
63#[derive(Debug, Clone)]
64pub struct CallSubquery {
65    pub input: PlanNodeId,
66    pub inner: PlanNodeId,
67    pub new_vars: Vec<VarId>,
68}
69
70/// Assembles a path value from matched node and relationship VarIds.
71#[derive(Debug, Clone)]
72pub struct PathBuild {
73    pub input: PlanNodeId,
74    /// VarId to store the assembled path.
75    pub output: VarId,
76    /// Node VarIds in order: head, chain[0].node, chain[1].node, ...
77    pub node_vars: Vec<VarId>,
78    /// Relationship VarIds in order: chain[0].rel, chain[1].rel, ...
79    pub rel_vars: Vec<VarId>,
80    /// `None` = normal path, `Some(false)` = shortestPath, `Some(true)` = allShortestPaths
81    pub shortest_path_all: Option<bool>,
82}
83
84/// Left-outer-join style node: runs the inner sub-plan for each input row.
85/// If no rows are produced, emits one row with nulls for the new variables.
86#[derive(Debug, Clone)]
87pub struct OptionalMatch {
88    /// Upstream rows that feed the optional match.
89    pub input: PlanNodeId,
90    /// The root of the inner sub-plan that implements the pattern + filter.
91    pub inner: PlanNodeId,
92    /// Variables introduced by the optional match (need null-extension).
93    pub new_vars: Vec<VarId>,
94}
95
96#[derive(Debug, Clone)]
97pub struct Argument;
98
99#[derive(Debug, Clone)]
100pub struct NodeScan {
101    pub input: Option<PlanNodeId>,
102    pub var: VarId,
103    /// Each inner Vec is a disjunctive group (OR). Outer Vec is conjunctive (AND).
104    pub labels: Vec<Vec<String>>,
105}
106
107#[derive(Debug, Clone)]
108pub struct NodeByPropertyScan {
109    pub input: Option<PlanNodeId>,
110    pub var: VarId,
111    /// Each inner Vec is a disjunctive group (OR). Outer Vec is conjunctive (AND).
112    pub labels: Vec<Vec<String>>,
113    pub key: String,
114    pub value: ResolvedExpr,
115}
116
117/// Range-bounded property scan rewritten from `Filter(NodeScan, var.prop CMP value)`
118/// patterns. `lo == None` means `-∞`, `hi == None` means `+∞`. Inclusivity flags
119/// distinguish `>` from `>=` and `<` from `<=`. Both bounds combined cover
120/// `BETWEEN`-style queries (`a < x AND x <= b`).
121#[derive(Debug, Clone)]
122pub struct NodeByPropertyRangeScan {
123    pub input: Option<PlanNodeId>,
124    pub var: VarId,
125    pub labels: Vec<Vec<String>>,
126    pub key: String,
127    pub lo: Option<ResolvedExpr>,
128    pub lo_inclusive: bool,
129    pub hi: Option<ResolvedExpr>,
130    pub hi_inclusive: bool,
131}
132
133/// Trigram-backed property scan rewritten from `Filter(NodeScan, var.prop OP "literal")`
134/// where OP is `STARTS WITH`, `ENDS WITH`, or `CONTAINS`. The executor consults
135/// the trigram registry for candidates and re-verifies the predicate.
136#[derive(Debug, Clone)]
137pub struct NodeByTextScan {
138    pub input: Option<PlanNodeId>,
139    pub var: VarId,
140    pub labels: Vec<Vec<String>>,
141    pub key: String,
142    pub predicate: TextPredicate,
143    pub query: ResolvedExpr,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum TextPredicate {
148    StartsWith,
149    EndsWith,
150    Contains,
151}
152
153/// Spatial-index scan rewritten from `Filter(NodeScan, predicate)`
154/// where the predicate is `point.withinBBox(n.prop, ll, ur)` or
155/// `point.distance(n.prop, c) OP d`. Index probe is conservative;
156/// the executor refilters with the precise predicate (including the
157/// inclusivity of distance comparisons and the z-axis when the point
158/// is 3D).
159#[derive(Debug, Clone)]
160pub struct NodeByPointScan {
161    pub input: Option<PlanNodeId>,
162    pub var: VarId,
163    pub labels: Vec<Vec<String>>,
164    pub key: String,
165    pub predicate: PointPredicate,
166}
167
168#[derive(Debug, Clone)]
169pub enum PointPredicate {
170    WithinBBox {
171        lower_left: ResolvedExpr,
172        upper_right: ResolvedExpr,
173    },
174    WithinDistance {
175        center: ResolvedExpr,
176        max_distance: ResolvedExpr,
177        inclusive: bool,
178    },
179}
180
181/// Range-bounded relationship scan, the rel-side mirror of
182/// [`NodeByPropertyRangeScan`]. Produces one row per indexed
183/// relationship of `types`, binding `src`, `rel`, `dst` to the stored
184/// endpoints. The optimizer only emits this operator for patterns
185/// with anonymous endpoints (no upstream label/property constraints
186/// on src/dst), since the operator does not refilter endpoints.
187#[derive(Debug, Clone)]
188pub struct RelByPropertyRangeScan {
189    pub input: Option<PlanNodeId>,
190    pub src: VarId,
191    pub rel: VarId,
192    pub dst: VarId,
193    pub types: Vec<String>,
194    pub direction: Direction,
195    pub key: String,
196    pub lo: Option<ResolvedExpr>,
197    pub lo_inclusive: bool,
198    pub hi: Option<ResolvedExpr>,
199    pub hi_inclusive: bool,
200}
201
202/// Trigram-backed relationship scan. Mirror of [`NodeByTextScan`].
203#[derive(Debug, Clone)]
204pub struct RelByTextScan {
205    pub input: Option<PlanNodeId>,
206    pub src: VarId,
207    pub rel: VarId,
208    pub dst: VarId,
209    pub types: Vec<String>,
210    pub direction: Direction,
211    pub key: String,
212    pub predicate: TextPredicate,
213    pub query: ResolvedExpr,
214}
215
216/// Spatial-index relationship scan. Mirror of [`NodeByPointScan`].
217#[derive(Debug, Clone)]
218pub struct RelByPointScan {
219    pub input: Option<PlanNodeId>,
220    pub src: VarId,
221    pub rel: VarId,
222    pub dst: VarId,
223    pub types: Vec<String>,
224    pub direction: Direction,
225    pub key: String,
226    pub predicate: PointPredicate,
227}
228
229#[derive(Debug, Clone)]
230pub struct Expand {
231    pub input: PlanNodeId,
232    pub src: VarId,
233    pub rel: Option<VarId>,
234    pub dst: VarId,
235    pub types: Vec<String>,
236    pub direction: Direction,
237    pub rel_properties: Option<ResolvedExpr>,
238    pub range: Option<RangeLiteral>,
239}
240
241#[derive(Debug, Clone)]
242pub struct Filter {
243    pub input: PlanNodeId,
244    pub predicate: ResolvedExpr,
245}
246
247#[derive(Debug, Clone)]
248pub struct Projection {
249    pub input: PlanNodeId,
250    pub distinct: bool,
251    pub items: Vec<ResolvedProjection>,
252    pub include_existing: bool,
253}
254
255#[derive(Debug, Clone)]
256pub struct Unwind {
257    pub input: PlanNodeId,
258    pub expr: ResolvedExpr,
259    pub alias: VarId,
260}
261
262#[derive(Debug, Clone)]
263pub struct Aggregation {
264    pub input: PlanNodeId,
265    pub group_by: Vec<ResolvedProjection>,
266    pub aggregates: Vec<ResolvedProjection>,
267}
268
269#[derive(Debug, Clone)]
270pub struct Sort {
271    pub input: PlanNodeId,
272    pub items: Vec<ResolvedSortItem>,
273    /// Optional upper bound for rows the sort must retain because a parent
274    /// LIMIT will discard everything after this many sorted rows.
275    pub top_k: Option<usize>,
276}
277
278#[derive(Debug, Clone)]
279pub struct Limit {
280    pub input: PlanNodeId,
281    pub skip: Option<ResolvedExpr>,
282    pub limit: Option<ResolvedExpr>,
283}
284
285#[derive(Debug, Clone)]
286pub struct Create {
287    pub input: PlanNodeId,
288    pub pattern: ResolvedPattern,
289}
290
291#[derive(Debug, Clone)]
292pub struct Merge {
293    pub input: PlanNodeId,
294    pub pattern_part: ResolvedPatternPart,
295    pub actions: Vec<ResolvedMergeAction>,
296}
297
298#[derive(Debug, Clone)]
299pub struct Delete {
300    pub input: PlanNodeId,
301    pub detach: bool,
302    pub expressions: Vec<ResolvedExpr>,
303}
304
305#[derive(Debug, Clone)]
306pub struct Set {
307    pub input: PlanNodeId,
308    pub items: Vec<ResolvedSetItem>,
309}
310
311#[derive(Debug, Clone)]
312pub struct Remove {
313    pub input: PlanNodeId,
314    pub items: Vec<ResolvedRemoveItem>,
315}