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