Skip to main content

akar_planner/
logical_operator.rs

1//! Logical operator types for query planning.
2
3use akar_binder::bound_statement::BoundExpression;
4use akar_catalog::CatalogColumn;
5use akar_parser::ast::Expression;
6
7/// A logical operator in the query plan.
8/// A logical expressions scan operator that reads correlated variables
9/// from an outer accumulate (for correlated subquery execution).
10///
11/// Ported from C++ `LogicalExpressionsScan`.
12#[derive(Debug, Clone)]
13pub struct LogicalExpressionsScan {
14    /// Names of the expressions/variables to scan from the outer context.
15    pub expressions: Vec<String>,
16    /// Index of the outer accumulate operator in the plan (set by optimizer).
17    pub outer_accumulate_idx: Option<usize>,
18    /// Estimated cardinality.
19    pub cardinality: u64,
20}
21
22/// A logical accumulate operator that materializes all input into memory.
23///
24/// Used as the build-side input for hash joins (via SIP/acc-hash-join)
25/// and for correlated subquery execution. Collects all input rows into
26/// an in-memory table for later probe.
27///
28/// Ported from C++ `LogicalAccumulate`.
29#[derive(Debug, Clone)]
30pub struct LogicalAccumulate {
31    /// Accumulate type (Regular or Optional).
32    pub accumulate_type: akar_common::enums::AccumulateType,
33    /// Expressions to flatten before accumulating.
34    pub flat_exprs: Vec<akar_parser::ast::Expression>,
35    /// Optional mark expression (for OPTIONAL MATCH).
36    pub mark: Option<akar_parser::ast::Expression>,
37    /// Child operator.
38    pub children: Vec<LogicalOperator>,
39    /// Estimated cardinality.
40    pub cardinality: u64,
41}
42
43/// Logical COUNT on a rel table — optimized via CSR metadata (Ladybug).
44#[derive(Debug, Clone)]
45pub struct LogicalCountRelTable {
46    pub table_name: String,
47    pub table_id: u64,
48}
49
50#[derive(Debug, Clone)]
51pub struct LogicalPartitioner {
52    pub children: Vec<LogicalOperator>,
53    pub cardinality: u64,
54}
55
56#[derive(Debug, Clone)]
57pub struct LogicalPathPropertyProbe {
58    pub children: Vec<LogicalOperator>,
59    pub cardinality: u64,
60    pub node_ids_col_idx: usize,
61    pub edge_ids_col_idx: Option<usize>,
62    pub properties: Vec<(String, bool, Vec<String>)>,
63}
64
65#[derive(Debug, Clone)]
66pub enum LogicalOperator {
67    ScanNode(LogicalScanNode),
68    ScanRel(LogicalScanRel),
69    VectorSimilarityScan(LogicalVectorSimilarityScan),
70    ArtIndexRangeScan(LogicalArtIndexRangeScan),
71    Filter(LogicalFilter),
72    Projection(LogicalProjection),
73    HashJoin(LogicalHashJoin),
74    CrossProduct(LogicalCrossProduct),
75    OrderBy(LogicalOrderBy),
76    Limit(LogicalLimit),
77    TopK(LogicalTopK),
78    Aggregate(LogicalAggregate),
79    Union(LogicalUnion),
80    Flatten(LogicalFlatten),
81    TableFunctionCall(LogicalTableFunctionCall),
82    StandaloneCall(LogicalStandaloneCall),
83    CopyFrom(LogicalCopyFrom),
84    BatchInsert(LogicalBatchInsert),
85    IndexLookup(LogicalIndexLookup),
86    Delete(LogicalDelete),
87    Set(LogicalSet),
88    OptionalMatch(LogicalOptionalMatch),
89    OptionalExtend(LogicalOptionalExtend),
90    Unwind(LogicalUnwind),
91    Foreach(LogicalForeach),
92    Merge(LogicalMerge),
93    MergeRel(LogicalMergeRel),
94    SemiJoin(LogicalSemiJoin),
95    AntiJoin(LogicalAntiJoin),
96    Intersect(LogicalIntersect),
97    Explain(LogicalExplain),
98    RecursiveExtend(LogicalRecursiveExtend),
99    Accumulate(LogicalAccumulate),
100    ExpressionsScan(LogicalExpressionsScan),
101    CountRelTable(LogicalCountRelTable),
102    Partitioner(LogicalPartitioner),
103    PathPropertyProbe(LogicalPathPropertyProbe),
104    // DDL operators
105    CreateNodeTable(LogicalCreateNodeTable),
106    CreateRelTable(LogicalCreateRelTable),
107    DropTable(LogicalDropTable),
108    AlterTable(LogicalAlterTable),
109    CreateIndex(LogicalCreateIndex),
110    DropIndex(LogicalDropIndex),
111    CreateVectorIndex(LogicalCreateVectorIndex),
112    CreateSequence(LogicalCreateSequence),
113    DropSequence(LogicalDropSequence),
114    CreateDml(LogicalCreateDml),
115    CreateNode(LogicalCreateNode),
116    CreateRel(LogicalCreateRel),
117    Extend(LogicalExtend),
118    ExportDatabase(LogicalExportDatabase),
119    ImportDatabase(LogicalImportDatabase),
120    CreateFtsIndex(LogicalCreateFtsIndex),
121    FtsScan(LogicalFtsScan),
122    EmptyResult(LogicalEmptyResult),
123    MultiplicityReducer(LogicalMultiplicityReducer),
124    Skip(LogicalSkip),
125    Insert(LogicalInsert),
126    ExtensionClause(LogicalExtensionClause),
127}
128
129impl LogicalOperator {
130    /// Get the estimated cardinality for this operator.
131    pub fn cardinality(&self) -> u64 {
132        match self {
133            LogicalOperator::ScanNode(s) => s.cardinality,
134            LogicalOperator::ScanRel(s) => s.cardinality,
135            LogicalOperator::VectorSimilarityScan(s) => s.cardinality,
136            LogicalOperator::ArtIndexRangeScan(s) => s.cardinality,
137            LogicalOperator::Filter(s) => s.cardinality,
138            LogicalOperator::Projection(s) => s.cardinality,
139            LogicalOperator::HashJoin(s) => s.cardinality,
140            LogicalOperator::CrossProduct(s) => s.cardinality,
141            LogicalOperator::OrderBy(s) => s.cardinality,
142            LogicalOperator::TopK(s) => s.cardinality,
143            LogicalOperator::Limit(s) => s.cardinality,
144            LogicalOperator::Aggregate(s) => s.cardinality,
145            LogicalOperator::Union(s) => s.cardinality,
146            LogicalOperator::Flatten(s) => s.cardinality,
147            LogicalOperator::TableFunctionCall(s) => s.cardinality,
148            LogicalOperator::StandaloneCall(s) => s.cardinality,
149            LogicalOperator::CopyFrom(s) => s.cardinality,
150            LogicalOperator::BatchInsert(s) => s.cardinality,
151            LogicalOperator::IndexLookup(s) => s.cardinality,
152            LogicalOperator::Delete(s) => s.cardinality,
153            LogicalOperator::Set(s) => s.cardinality,
154            LogicalOperator::OptionalMatch(s) => s.cardinality,
155            LogicalOperator::OptionalExtend(s) => s.cardinality,
156            LogicalOperator::Unwind(s) => s.cardinality,
157            LogicalOperator::Foreach(s) => s.cardinality,
158            LogicalOperator::Merge(s) => s.cardinality,
159            LogicalOperator::MergeRel(s) => s.cardinality,
160            LogicalOperator::SemiJoin(s) => s.cardinality,
161            LogicalOperator::AntiJoin(s) => s.cardinality,
162            LogicalOperator::Intersect(s) => s.cardinality,
163            LogicalOperator::Explain(s) => s.cardinality,
164            LogicalOperator::RecursiveExtend(s) => s.cardinality,
165            LogicalOperator::Accumulate(s) => s.cardinality,
166            LogicalOperator::ExpressionsScan(s) => s.cardinality,
167            LogicalOperator::CountRelTable(_) => 1,
168            LogicalOperator::Partitioner(s) => s.cardinality,
169            LogicalOperator::PathPropertyProbe(s) => s.cardinality,
170            // DDL operators
171            LogicalOperator::CreateNodeTable(s) => s.cardinality,
172            LogicalOperator::CreateRelTable(s) => s.cardinality,
173            LogicalOperator::DropTable(s) => s.cardinality,
174            LogicalOperator::AlterTable(s) => s.cardinality,
175            LogicalOperator::CreateIndex(s) => s.cardinality,
176            LogicalOperator::DropIndex(s) => s.cardinality,
177            LogicalOperator::CreateVectorIndex(s) => s.cardinality,
178            LogicalOperator::CreateSequence(s) => s.cardinality,
179            LogicalOperator::DropSequence(s) => s.cardinality,
180            LogicalOperator::CreateDml(s) => s.cardinality,
181            LogicalOperator::CreateNode(s) => s.cardinality,
182            LogicalOperator::CreateRel(s) => s.cardinality,
183            LogicalOperator::Extend(s) => s.cardinality,
184            LogicalOperator::ExportDatabase(s) => s.cardinality,
185            LogicalOperator::ImportDatabase(s) => s.cardinality,
186            LogicalOperator::CreateFtsIndex(s) => s.cardinality,
187            LogicalOperator::FtsScan(s) => s.cardinality,
188            LogicalOperator::EmptyResult(s) => s.cardinality,
189            LogicalOperator::MultiplicityReducer(s) => s.cardinality,
190            LogicalOperator::Skip(s) => s.cardinality,
191            LogicalOperator::Insert(s) => s.cardinality,
192            LogicalOperator::ExtensionClause(s) => s.cardinality,
193        }
194    }
195
196    /// Set the estimated cardinality for this operator.
197    pub fn set_cardinality(&mut self, card: u64) {
198        match self {
199            LogicalOperator::ScanNode(s) => s.cardinality = card,
200            LogicalOperator::ScanRel(s) => s.cardinality = card,
201            LogicalOperator::VectorSimilarityScan(s) => s.cardinality = card,
202            LogicalOperator::ArtIndexRangeScan(s) => s.cardinality = card,
203            LogicalOperator::Filter(s) => s.cardinality = card,
204            LogicalOperator::Projection(s) => s.cardinality = card,
205            LogicalOperator::HashJoin(s) => s.cardinality = card,
206            LogicalOperator::CrossProduct(s) => s.cardinality = card,
207            LogicalOperator::OrderBy(s) => s.cardinality = card,
208            LogicalOperator::TopK(s) => s.cardinality = card,
209            LogicalOperator::Limit(s) => s.cardinality = card,
210            LogicalOperator::Aggregate(s) => s.cardinality = card,
211            LogicalOperator::Union(s) => s.cardinality = card,
212            LogicalOperator::Flatten(s) => s.cardinality = card,
213            LogicalOperator::TableFunctionCall(s) => s.cardinality = card,
214            LogicalOperator::StandaloneCall(s) => s.cardinality = card,
215            LogicalOperator::CopyFrom(s) => s.cardinality = card,
216            LogicalOperator::BatchInsert(s) => s.cardinality = card,
217            LogicalOperator::IndexLookup(s) => s.cardinality = card,
218            LogicalOperator::Delete(s) => s.cardinality = card,
219            LogicalOperator::Set(s) => s.cardinality = card,
220            LogicalOperator::OptionalMatch(s) => s.cardinality = card,
221            LogicalOperator::OptionalExtend(s) => s.cardinality = card,
222            LogicalOperator::Unwind(s) => s.cardinality = card,
223            LogicalOperator::Foreach(s) => s.cardinality = card,
224            LogicalOperator::Merge(s) => s.cardinality = card,
225            LogicalOperator::MergeRel(s) => s.cardinality = card,
226            LogicalOperator::SemiJoin(s) => s.cardinality = card,
227            LogicalOperator::AntiJoin(s) => s.cardinality = card,
228            LogicalOperator::Intersect(s) => s.cardinality = card,
229            LogicalOperator::Explain(s) => s.cardinality = card,
230            LogicalOperator::RecursiveExtend(s) => s.cardinality = card,
231            LogicalOperator::Accumulate(s) => s.cardinality = card,
232            LogicalOperator::ExpressionsScan(s) => s.cardinality = card,
233            LogicalOperator::CountRelTable(_) => {}
234            LogicalOperator::Partitioner(s) => s.cardinality = card,
235            LogicalOperator::PathPropertyProbe(s) => s.cardinality = card,
236            // DDL operators
237            LogicalOperator::CreateNodeTable(s) => s.cardinality = card,
238            LogicalOperator::CreateRelTable(s) => s.cardinality = card,
239            LogicalOperator::DropTable(s) => s.cardinality = card,
240            LogicalOperator::AlterTable(s) => s.cardinality = card,
241            LogicalOperator::CreateIndex(s) => s.cardinality = card,
242            LogicalOperator::DropIndex(s) => s.cardinality = card,
243            LogicalOperator::CreateVectorIndex(s) => s.cardinality = card,
244            LogicalOperator::CreateSequence(s) => s.cardinality = card,
245            LogicalOperator::DropSequence(s) => s.cardinality = card,
246            LogicalOperator::CreateDml(s) => s.cardinality = card,
247            LogicalOperator::CreateNode(s) => s.cardinality = card,
248            LogicalOperator::CreateRel(s) => s.cardinality = card,
249            LogicalOperator::Extend(s) => s.cardinality = card,
250            LogicalOperator::ExportDatabase(s) => s.cardinality = card,
251            LogicalOperator::ImportDatabase(s) => s.cardinality = card,
252            LogicalOperator::CreateFtsIndex(s) => s.cardinality = card,
253            LogicalOperator::FtsScan(s) => s.cardinality = card,
254            LogicalOperator::EmptyResult(s) => s.cardinality = card,
255            LogicalOperator::MultiplicityReducer(s) => s.cardinality = card,
256            LogicalOperator::Skip(s) => s.cardinality = card,
257            LogicalOperator::Insert(s) => s.cardinality = card,
258            LogicalOperator::ExtensionClause(s) => s.cardinality = card,
259        }
260    }
261
262    /// Recursively visit all operators in the tree bottom-up.
263    pub fn visit_bottom_up<F: FnMut(&mut LogicalOperator)>(op: &mut LogicalOperator, f: &mut F) {
264        let children = op.children_mut();
265        for child in children {
266            Self::visit_bottom_up(child, f);
267        }
268        f(op);
269    }
270
271    /// Get mutable references to all direct children of this operator.
272    pub fn children_mut(&mut self) -> Vec<&mut LogicalOperator> {
273        match self {
274            LogicalOperator::Filter(s) => s.children.iter_mut().collect(),
275            LogicalOperator::Projection(s) => s.children.iter_mut().collect(),
276            LogicalOperator::HashJoin(s) => vec![&mut *s.probe_side, &mut *s.build_side],
277            LogicalOperator::CrossProduct(s) => vec![&mut *s.left, &mut *s.right],
278            LogicalOperator::OrderBy(s) => s.children.iter_mut().collect(),
279            LogicalOperator::TopK(s) => s.children.iter_mut().collect(),
280            LogicalOperator::Limit(s) => s.children.iter_mut().collect(),
281            LogicalOperator::Aggregate(s) => s.children.iter_mut().collect(),
282            LogicalOperator::Union(s) => vec![&mut *s.left, &mut *s.right],
283            LogicalOperator::Flatten(s) => s.children.iter_mut().collect(),
284            LogicalOperator::OptionalMatch(s) => vec![&mut *s.left, &mut *s.right],
285            LogicalOperator::OptionalExtend(s) => s.children.iter_mut().collect(),
286            LogicalOperator::SemiJoin(s) => vec![&mut *s.left, &mut *s.right],
287            LogicalOperator::AntiJoin(s) => vec![&mut *s.left, &mut *s.right],
288            LogicalOperator::Intersect(s) => vec![&mut *s.left, &mut *s.right],
289            LogicalOperator::Explain(s) => vec![&mut *s.inner],
290            LogicalOperator::RecursiveExtend(_) => vec![],
291            LogicalOperator::Accumulate(s) => s.children.iter_mut().collect(),
292            LogicalOperator::Partitioner(s) => s.children.iter_mut().collect(),
293            LogicalOperator::PathPropertyProbe(s) => s.children.iter_mut().collect(),
294            LogicalOperator::CountRelTable(_) => vec![],
295            LogicalOperator::ExpressionsScan(_) => vec![],
296            LogicalOperator::TableFunctionCall(_) => vec![],
297            LogicalOperator::StandaloneCall(_) => vec![],
298            LogicalOperator::CopyFrom(_)
299            | LogicalOperator::BatchInsert(_)
300            | LogicalOperator::IndexLookup(_)
301            | LogicalOperator::Delete(_)
302            | LogicalOperator::Set(_)
303            | LogicalOperator::Unwind(_)
304            | LogicalOperator::Foreach(_)
305            | LogicalOperator::Merge(_)
306            | LogicalOperator::MergeRel(_) => vec![],
307            // Leaf operators have no children
308            LogicalOperator::ArtIndexRangeScan(_)
309            | LogicalOperator::VectorSimilarityScan(_)
310            | LogicalOperator::ScanNode(_)
311            | LogicalOperator::ScanRel(_)
312            | LogicalOperator::CreateNodeTable(_)
313            | LogicalOperator::CreateRelTable(_)
314            | LogicalOperator::DropTable(_)
315            | LogicalOperator::AlterTable(_)
316            | LogicalOperator::CreateIndex(_)
317            | LogicalOperator::DropIndex(_)
318            | LogicalOperator::CreateVectorIndex(_)
319            | LogicalOperator::CreateSequence(_)
320            | LogicalOperator::DropSequence(_)
321            | LogicalOperator::CreateDml(_)
322            | LogicalOperator::CreateNode(_)
323            | LogicalOperator::CreateRel(_)
324            | LogicalOperator::Extend(_)
325            | LogicalOperator::ExportDatabase(_)
326            | LogicalOperator::ImportDatabase(_)
327            | LogicalOperator::CreateFtsIndex(_)
328            | LogicalOperator::FtsScan(_)
329            | LogicalOperator::EmptyResult(_)
330            | LogicalOperator::Insert(_)
331            | LogicalOperator::ExtensionClause(_) => vec![],
332            LogicalOperator::MultiplicityReducer(s) => s.children.iter_mut().collect(),
333            LogicalOperator::Skip(s) => s.children.iter_mut().collect(),
334        }
335    }
336
337    /// Get the child operators (immutable).
338    pub fn children(&self) -> Vec<&LogicalOperator> {
339        match self {
340            LogicalOperator::Filter(s) => s.children.iter().collect(),
341            LogicalOperator::Projection(s) => s.children.iter().collect(),
342            LogicalOperator::HashJoin(s) => vec![&*s.probe_side, &*s.build_side],
343            LogicalOperator::CrossProduct(s) => vec![&*s.left, &*s.right],
344            LogicalOperator::OrderBy(s) => s.children.iter().collect(),
345            LogicalOperator::TopK(s) => s.children.iter().collect(),
346            LogicalOperator::Limit(s) => s.children.iter().collect(),
347            LogicalOperator::Aggregate(s) => s.children.iter().collect(),
348            LogicalOperator::Union(s) => vec![&*s.left, &*s.right],
349            LogicalOperator::Flatten(s) => s.children.iter().collect(),
350            LogicalOperator::OptionalMatch(s) => vec![&*s.left, &*s.right],
351            LogicalOperator::OptionalExtend(s) => s.children.iter().collect(),
352            LogicalOperator::SemiJoin(s) => vec![&*s.left, &*s.right],
353            LogicalOperator::AntiJoin(s) => vec![&*s.left, &*s.right],
354            LogicalOperator::Intersect(s) => vec![&*s.left, &*s.right],
355            LogicalOperator::Explain(s) => vec![&*s.inner],
356            LogicalOperator::RecursiveExtend(_) => vec![],
357            LogicalOperator::Accumulate(s) => s.children.iter().collect(),
358            LogicalOperator::Partitioner(s) => s.children.iter().collect(),
359            LogicalOperator::PathPropertyProbe(s) => s.children.iter().collect(),
360            LogicalOperator::CountRelTable(_) => vec![],
361            LogicalOperator::ExpressionsScan(_) => vec![],
362            LogicalOperator::TableFunctionCall(_) => vec![],
363            LogicalOperator::StandaloneCall(_) => vec![],
364            LogicalOperator::CopyFrom(_)
365            | LogicalOperator::BatchInsert(_)
366            | LogicalOperator::IndexLookup(_)
367            | LogicalOperator::Delete(_)
368            | LogicalOperator::Set(_)
369            | LogicalOperator::Unwind(_)
370            | LogicalOperator::Foreach(_)
371            | LogicalOperator::Merge(_)
372            | LogicalOperator::MergeRel(_) => vec![],
373            LogicalOperator::ArtIndexRangeScan(_)
374            | LogicalOperator::VectorSimilarityScan(_)
375            | LogicalOperator::ScanNode(_)
376            | LogicalOperator::ScanRel(_)
377            | LogicalOperator::CreateNodeTable(_)
378            | LogicalOperator::CreateRelTable(_)
379            | LogicalOperator::DropTable(_)
380            | LogicalOperator::AlterTable(_)
381            | LogicalOperator::CreateIndex(_)
382            | LogicalOperator::DropIndex(_)
383            | LogicalOperator::CreateVectorIndex(_)
384            | LogicalOperator::CreateSequence(_)
385            | LogicalOperator::DropSequence(_)
386            | LogicalOperator::CreateDml(_)
387            | LogicalOperator::CreateNode(_)
388            | LogicalOperator::CreateRel(_)
389            | LogicalOperator::Extend(_)
390            | LogicalOperator::ExportDatabase(_)
391            | LogicalOperator::ImportDatabase(_)
392            | LogicalOperator::CreateFtsIndex(_)
393            | LogicalOperator::FtsScan(_)
394            | LogicalOperator::EmptyResult(_)
395            | LogicalOperator::Insert(_)
396            | LogicalOperator::ExtensionClause(_) => vec![],
397            LogicalOperator::MultiplicityReducer(s) => s.children.iter().collect(),
398            LogicalOperator::Skip(s) => s.children.iter().collect(),
399        }
400    }
401}
402
403#[derive(Debug, Clone)]
404pub struct LogicalArtIndexRangeScan {
405    pub table_name: String,
406    pub table_id: u64,
407    pub alias: Option<String>,
408    pub lower_bound: Option<akar_common::types::Value>,
409    pub upper_bound: Option<akar_common::types::Value>,
410    pub lower_inclusive: bool,
411    pub upper_inclusive: bool,
412    pub cardinality: u64,
413}
414
415#[derive(Debug, Clone)]
416pub struct LogicalVectorSimilarityScan {
417    pub table_name: String,
418    pub column_name: String,
419    pub query_vector: Vec<f64>,
420    pub top_k: u64,
421    /// Node alias for field-name prefixing (e.g. `n` in
422    /// `MATCH (n:Memory) ...`). `None` for the bare `CALL vector_similarity_scan`
423    /// path, which emits unprefixed output columns like `distance`/`_id`.
424    pub alias: Option<String>,
425    pub cardinality: u64,
426}
427
428#[derive(Debug, Clone)]
429pub struct LogicalScanNode {
430    pub table_name: String,
431    pub table_id: u64,
432    pub alias: Option<String>,
433    pub columns: Vec<String>,
434    pub cardinality: u64,
435    pub fts_query: Option<LogicalFtsScan>,
436    pub predicate: Option<Expression>,
437}
438
439#[derive(Debug, Clone)]
440pub struct LogicalScanRel {
441    pub table_name: String,
442    pub table_id: u64,
443    pub direction: akar_parser::ast::EdgeDirection,
444    pub cardinality: u64,
445}
446
447#[derive(Debug, Clone)]
448pub struct LogicalFilter {
449    pub expression: Expression,
450    pub children: Vec<LogicalOperator>,
451    pub cardinality: u64,
452}
453
454#[derive(Debug, Clone)]
455pub struct LogicalProjection {
456    pub expressions: Vec<BoundExpression>,
457    pub children: Vec<LogicalOperator>,
458    pub cardinality: u64,
459}
460
461#[derive(Debug, Clone)]
462pub struct LogicalHashJoin {
463    pub join_keys: Vec<Expression>,
464    pub build_side: Box<LogicalOperator>,
465    pub probe_side: Box<LogicalOperator>,
466    pub cardinality: u64,
467    /// Whether this join is eligible for foreign join push-down optimization.
468    /// Set by the ForeignJoinPushDown optimizer pass when all tables in the
469    /// pattern belong to the same foreign database.
470    pub push_down_eligible: bool,
471}
472
473/// Semi-join: returns left rows that have a matching key in the right side.
474/// Like HashJoin but only emits left columns for matching rows.
475#[derive(Debug, Clone)]
476pub struct LogicalSemiJoin {
477    pub join_keys: Vec<Expression>,
478    pub left: Box<LogicalOperator>,
479    pub right: Box<LogicalOperator>,
480    pub cardinality: u64,
481}
482
483/// Anti-join: returns left rows that have NO matching key in the right side.
484/// Like SemiJoin but inverts the match condition.
485#[derive(Debug, Clone)]
486pub struct LogicalAntiJoin {
487    pub join_keys: Vec<Expression>,
488    pub left: Box<LogicalOperator>,
489    pub right: Box<LogicalOperator>,
490    pub cardinality: u64,
491}
492
493/// EXPLAIN operator — wraps a child plan and produces a textual plan description.
494///
495/// Unlike other operators, Explain does not execute its child; instead it
496/// serializes the operator tree to a human-readable string.
497#[derive(Debug, Clone)]
498pub struct LogicalExplain {
499    /// The inner operator tree to explain.
500    pub inner: Box<LogicalOperator>,
501    /// The type of explain output.
502    pub explain_type: akar_parser::ast::ExplainType,
503    /// Cardinality (always 1 — one row with the plan string).
504    pub cardinality: u64,
505}
506
507#[derive(Debug, Clone)]
508pub struct LogicalCrossProduct {
509    pub left: Box<LogicalOperator>,
510    pub right: Box<LogicalOperator>,
511    pub cardinality: u64,
512}
513
514#[derive(Debug, Clone)]
515pub struct LogicalOrderBy {
516    pub sort_keys: Vec<(Expression, bool)>, // (expression, ascending)
517    pub children: Vec<LogicalOperator>,
518    pub cardinality: u64,
519}
520
521/// A fused ORDER BY + LIMIT operator for Top-K optimization.
522///
523/// When the optimizer detects a consecutive ORDER BY followed by LIMIT,
524/// it fuses them into a single LogicalTopK. This signals the processor
525/// to use a BinaryHeap-based TopK execution (O(n log k)) instead of
526/// full sort + limit (O(n log n)).
527#[derive(Debug, Clone)]
528pub struct LogicalTopK {
529    pub sort_keys: Vec<(Expression, bool)>,
530    pub limit: u64,
531    pub offset: u64,
532    pub children: Vec<LogicalOperator>,
533    pub cardinality: u64,
534}
535
536#[derive(Debug, Clone)]
537pub struct LogicalLimit {
538    pub limit: u64,
539    pub offset: u64,
540    pub children: Vec<LogicalOperator>,
541    pub cardinality: u64,
542}
543
544#[derive(Debug, Clone)]
545pub struct LogicalAggregate {
546    pub group_by: Vec<Expression>,
547    pub aggregates: Vec<(String, Vec<Expression>)>, // (function_name, args)
548    pub children: Vec<LogicalOperator>,
549    pub cardinality: u64,
550}
551
552#[derive(Debug, Clone)]
553pub struct LogicalUnion {
554    pub left: Box<LogicalOperator>,
555    pub right: Box<LogicalOperator>,
556    pub all: bool,
557    pub cardinality: u64,
558}
559
560/// A flatten operator that converts a specific factorization group from
561/// unflat (list-like) to flat (scalar) representation.
562///
563/// Inserted by `FactorizationRewriting` to ensure operators like HashJoin
564/// receive the correct factorization layout.
565#[derive(Debug, Clone)]
566pub struct LogicalFlatten {
567    pub group_pos: usize,
568    pub children: Vec<LogicalOperator>,
569    pub cardinality: u64,
570}
571
572/// UNWIND operator — expands a list expression into rows.
573#[derive(Debug, Clone)]
574pub struct LogicalUnwind {
575    pub expression: akar_parser::ast::Expression,
576    pub variable: String,
577    pub cardinality: u64,
578}
579
580/// INTERSECT operator — finds common keys across multiple build sides.
581///
582/// Used for multi-pattern matching like `MATCH (a)-[:r1]->(b), (a)-[:r2]->(c)`
583/// where `a` is the shared key. Intersect probes multiple build hash tables
584/// and outputs combined payloads only for keys present in all build sides.
585#[derive(Debug, Clone)]
586pub struct LogicalIntersect {
587    /// Number of build sides (hash tables to probe).
588    pub num_build_sides: u32,
589    /// Key expressions for each build side.
590    pub build_key_exprs: Vec<Expression>,
591    /// Probe side — produces the shared key value to look up.
592    pub left: Box<LogicalOperator>,
593    /// Build side — produces hash tables for each pattern.
594    pub right: Box<LogicalOperator>,
595    /// Estimated cardinality.
596    pub cardinality: u64,
597}
598
599/// Variable-length path (recursive extend) operator.
600///
601/// Corresponds to `MATCH (a)-[e*1..3]->(b)` — traverses the graph
602/// up to `upper_bound` hops from source nodes and produces results
603/// for each path whose length is between `lower_bound` and `upper_bound`.
604///
605/// Supports both unweighted BFS and weighted shortest path (Dijkstra)
606/// when `weight_property` is specified.
607///
608/// This is a leaf operator that executes BFS/Dijkstra traversal during query execution.
609#[derive(Debug, Clone)]
610pub struct LogicalRecursiveExtend {
611    /// Source node variable name.
612    pub source_var: String,
613    /// Source node table ID.
614    pub source_table_id: u64,
615    /// Edge variable name (optional).
616    pub edge_var: Option<String>,
617    /// Destination node variable name.
618    pub target_var: String,
619    /// Relationship table ID(s) to traverse.
620    pub rel_table_ids: Vec<u64>,
621    /// Relationship label(s).
622    pub rel_labels: Vec<String>,
623    /// Minimum path length.
624    pub lower_bound: u64,
625    /// Maximum path length.
626    pub upper_bound: u64,
627    /// Traversal direction.
628    pub direction: akar_common::enums::ExtendDirection,
629    /// Path semantic (WALK / TRAIL / ACYCLIC).
630    pub semantic: akar_common::enums::PathSemantic,
631    /// Optional edge weight property name for weighted shortest path.
632    /// When `Some(prop_name)`, traversal uses Dijkstra's algorithm instead of BFS,
633    /// and results are sorted by cumulative path cost.
634    pub weight_property: Option<String>,
635    /// Optional name for the cost output column (e.g., "cost" or "totalWeight").
636    /// Only used when `weight_property` is set.
637    pub cost_output_name: Option<String>,
638    /// Estimated cardinality.
639    pub cardinality: u64,
640}
641
642/// OPTIONAL MATCH operator — a tree node with required (left) and optional (right) children.
643///
644/// The required side is executed first. For each resulting row, the optional
645/// side is attempted. If the optional side produces a match, the combined
646/// row (left + right columns) is emitted. If no match, left columns are
647/// emitted with NULLs for right-side columns.
648#[derive(Debug, Clone)]
649pub struct LogicalOptionalMatch {
650    pub left: Box<LogicalOperator>,
651    pub right: Box<LogicalOperator>,
652    pub cardinality: u64,
653}
654
655/// OPTIONAL MATCH over an already-bound pair of node variables.
656///
657/// Used when both endpoint node variables of the optional pattern are bound by
658/// the required side (e.g. `OPTIONAL MATCH (a)-[existing:Connected]-(b)` where
659/// `a` and `b` are already in scope). Unlike `LogicalOptionalMatch`, the right
660/// side is not a scan: the pattern is probed per input row against the
661/// relationship table adjacency (forward and/or reverse), emitting the edge
662/// property columns, or NULL-padding them when no edge exists (P53.25).
663#[derive(Debug, Clone)]
664pub struct LogicalOptionalExtend {
665    pub children: Vec<LogicalOperator>,
666    /// Name of the relationship table to probe.
667    pub rel_table_name: String,
668    /// ID of the relationship table.
669    pub rel_table_id: u64,
670    /// Variable name of the relationship (e.g., "existing"); prefix for the
671    /// emitted edge property columns.
672    pub rel_var: String,
673    /// Variable name of the bound source node (e.g., "a").
674    pub src_node_var: String,
675    /// Variable name of the bound destination node (e.g., "b"). An empty
676    /// string selects fan-out mode (P53.40): the destination is anonymous
677    /// (`OPTIONAL MATCH (m)-[r:R]-(:T)`), so every edge incident on the source
678    /// becomes one output row.
679    pub dst_node_var: String,
680    /// Direction of the probe (forward, backward, or both).
681    pub direction: akar_parser::ast::EdgeDirection,
682    /// Estimated cardinality.
683    pub cardinality: u64,
684}
685
686/// SET operator — updates properties on matched rows.
687#[derive(Debug, Clone)]
688pub struct LogicalSet {
689    pub table_name: String,
690    pub table_id: u64,
691    pub is_node: bool,
692    /// All assignments of the SET clause, evaluated against the pre-update
693    /// snapshot as a group (P53.17). Multiple items were previously emitted as
694    /// a chain of single-item operators, so items after the first received the
695    /// previous item's count chunk and lost the scan rows.
696    pub items: Vec<SetItem>,
697    /// True when the SET is the terminal clause (no trailing RETURN): the
698    /// operator reports "N rows updated" as column 0. False when a RETURN
699    /// follows — an empty match must flow 0 rows, not a phantom count row
700    /// (P53.39, kairos Finding P59.1).
701    pub emit_count: bool,
702    pub cardinality: u64,
703}
704
705/// A single `SET n.prop = <expr>` assignment inside a [`LogicalSet`].
706#[derive(Debug, Clone)]
707pub struct SetItem {
708    pub column_name: String,
709    pub column_idx: usize,
710    pub value: akar_parser::ast::Expression,
711}
712
713/// DELETE operator — removes rows from a table.
714#[derive(Debug, Clone)]
715pub struct LogicalDelete {
716    pub table_name: String,
717    pub table_id: u64,
718    pub primary_key_column: String,
719    pub is_node: bool,
720    pub detach: bool,
721    pub cardinality: u64,
722}
723
724/// COPY FROM operator — loads data from a file into a table.
725/// This is a leaf-level DML operator (no children) that the processor
726/// resolves into a `PhysicalCopyFrom` for execution.
727#[derive(Debug, Clone)]
728pub struct LogicalCopyFrom {
729    pub table_name: String,
730    pub table_id: u64,
731    pub file_path: String,
732    pub options: std::collections::HashMap<String, String>,
733    pub cardinality: u64,
734}
735
736/// Batch insert operator — inserts multiple rows/rels in a single operation.
737///
738/// Unlike `CopyFrom` which reads from a file, `BatchInsert` takes pre-collected
739/// data from the plan pipeline (e.g., multiple fused CREATE statements).
740/// Uses `NodeTable::insert_rows_batch()` / `RelTable::insert_rels_batch()`.
741#[derive(Debug, Clone)]
742pub struct LogicalBatchInsert {
743    pub table_name: String,
744    pub table_id: u64,
745    /// Rows to insert: each row is a Vec<Value> matching column order.
746    pub rows: Vec<Vec<akar_common::types::Value>>,
747    pub cardinality: u64,
748}
749
750/// Index lookup operator — point lookup via ART index on a PK column.
751#[derive(Debug, Clone)]
752pub struct LogicalIndexLookup {
753    pub table_name: String,
754    pub table_id: u64,
755    pub key_value: akar_common::types::Value,
756    pub cardinality: u64,
757}
758
759/// FOREACH operator — iterates over list elements and executes sub-plans.
760#[derive(Debug, Clone)]
761pub struct LogicalForeach {
762    pub variable: String,
763    pub expression: akar_parser::ast::Expression,
764    /// Sub-plans to execute for each list element.
765    pub sub_plans: Vec<Vec<LogicalOperator>>,
766    pub cardinality: u64,
767}
768
769/// A table function call operator.
770///
771/// Invokes a registered table function (e.g., `duckdb_scan`, `delta_scan`)
772/// and produces a DataChunk as output. The function is looked up by name
773/// in the FunctionRegistry during execution.
774#[derive(Debug, Clone)]
775pub struct LogicalTableFunctionCall {
776    pub function_name: String,
777    pub args: Vec<akar_parser::ast::Expression>,
778    pub cardinality: u64,
779}
780
781#[derive(Debug, Clone)]
782pub struct LogicalStandaloneCall {
783    pub function_name: String,
784    pub args: Vec<akar_parser::ast::Expression>,
785    pub cardinality: u64,
786}
787
788/// MERGE operator — match or create a node/pattern.
789///
790/// The processor first attempts to match a node with the given properties.
791/// If found, applies `ON MATCH SET` operations. If not found, creates a
792/// new node with the pattern properties and applies `ON CREATE SET`.
793#[derive(Debug, Clone)]
794pub struct LogicalMerge {
795    pub table_name: String,
796    pub table_id: u64,
797    /// Properties from the MERGE pattern (name, expression pairs).
798    pub properties: Vec<(String, akar_parser::ast::Expression)>,
799    /// SET operations to apply when the node already exists (matched).
800    pub on_match: Vec<LogicalSet>,
801    /// SET operations to apply when a new node is created.
802    pub on_create: Vec<LogicalSet>,
803    pub cardinality: u64,
804}
805
806/// Logical operator for edge MERGE (P53.20): `MERGE (a)-[r:R]->(b)`.
807///
808/// Matches an existing edge from `src` to `dst` on the rel table whose props
809/// equal the pattern properties; if absent, inserts a new edge. Emits the
810/// matched/inserted edge's `_id` as `<edge_var>._id` so a following SET clause
811/// can target it.
812#[derive(Debug, Clone)]
813pub struct LogicalMergeRel {
814    pub rel_table_name: String,
815    pub rel_table_id: u64,
816    /// Variable bound to the edge (e.g. `r`).
817    pub edge_var: String,
818    /// Node variables bound by a prior MATCH that anchor the endpoints.
819    pub src_node_var: String,
820    pub dst_node_var: String,
821    /// Inline properties from the edge pattern (`{type: $type}`).
822    pub properties: Vec<(String, akar_parser::ast::Expression)>,
823    /// SET operations applied when the edge already exists (empty for the
824    /// standalone-SET form used by Kairos `add_connection`).
825    pub on_match: Vec<LogicalSet>,
826    /// SET operations applied when a new edge is created.
827    pub on_create: Vec<LogicalSet>,
828    pub cardinality: u64,
829}
830
831// ==================== DDL Operators ====================
832
833/// Logical operator for CREATE NODE TABLE.
834#[derive(Debug, Clone)]
835pub struct LogicalCreateNodeTable {
836    pub name: String,
837    pub columns: Vec<CatalogColumn>,
838    pub primary_key: String,
839    pub if_not_exists: bool,
840    pub cardinality: u64,
841}
842
843/// Logical operator for CREATE REL TABLE.
844#[derive(Debug, Clone)]
845pub struct LogicalCreateRelTable {
846    pub name: String,
847    pub from: String,
848    pub to: String,
849    pub columns: Vec<CatalogColumn>,
850    pub if_not_exists: bool,
851    pub cardinality: u64,
852}
853
854/// Logical operator for DROP TABLE.
855#[derive(Debug, Clone)]
856pub struct LogicalDropTable {
857    pub name: String,
858    pub cardinality: u64,
859}
860
861/// Logical operator for ALTER TABLE.
862#[derive(Debug, Clone)]
863pub struct LogicalAlterTable {
864    pub table_name: String,
865    pub action: akar_parser::ast::AlterAction,
866    pub cardinality: u64,
867}
868
869/// Logical operator for CREATE [ART|HASH] INDEX.
870#[derive(Debug, Clone)]
871pub struct LogicalCreateIndex {
872    pub index_type: akar_catalog::IndexType,
873    pub index_name: String,
874    pub table_name: String,
875    pub column_name: String,
876    pub cardinality: u64,
877}
878
879/// Logical operator for DROP INDEX.
880#[derive(Debug, Clone)]
881pub struct LogicalDropIndex {
882    pub index_name: String,
883    pub table_name: String,
884    pub cardinality: u64,
885}
886
887/// Logical operator for CREATE VECTOR INDEX.
888#[derive(Debug, Clone)]
889pub struct LogicalCreateVectorIndex {
890    pub index_name: String,
891    pub table_name: String,
892    pub column_name: String,
893    pub metric: String,
894    pub dimensions: u64,
895    pub cardinality: u64,
896}
897
898/// Logical operator for CREATE SEQUENCE.
899#[derive(Debug, Clone)]
900pub struct LogicalCreateSequence {
901    pub name: String,
902    pub if_not_exists: bool,
903    pub or_replace: bool,
904    pub start_with: i64,
905    pub increment: i64,
906    pub min_value: i64,
907    pub max_value: i64,
908    pub cycle: bool,
909    pub cardinality: u64,
910}
911
912/// Logical operator for DROP SEQUENCE.
913#[derive(Debug, Clone)]
914pub struct LogicalDropSequence {
915    pub name: String,
916    pub if_exists: bool,
917    pub cardinality: u64,
918}
919
920/// Logical operator for CREATE DML (node creation with properties).
921#[derive(Debug, Clone)]
922pub struct LogicalCreateDml {
923    pub table_name: String,
924    pub table_id: u64,
925    pub properties: Vec<(String, akar_parser::ast::Expression)>,
926    pub cardinality: u64,
927}
928
929#[derive(Debug, Clone)]
930pub struct LogicalCreateNode {
931    pub table_name: String,
932    pub table_id: u64,
933    pub out_var_name: String,
934    pub properties: Vec<(String, akar_parser::ast::Expression)>,
935    pub cardinality: u64,
936}
937
938/// Logical operator for extending from a source node through a relationship.
939///
940/// Replaces the combination of ScanRel + ScanNode(dest) in the pipeline.
941/// For each source node, looks up adjacency list entries in the rel table,
942/// producing output rows that include source fields, rel properties, and
943/// destination node properties.
944///
945/// Ported from C++ `LogicalExtend`.
946#[derive(Debug, Clone)]
947pub struct LogicalExtend {
948    /// Name of the relationship table to extend through.
949    pub rel_table_name: String,
950    /// ID of the relationship table.
951    pub rel_table_id: u64,
952    /// Variable name of the relationship (e.g., "r" in `-[r:RELATES_TO]->`).
953    /// Used as the field-name prefix for relationship properties.
954    pub rel_var: String,
955    /// Variable name of the bound (source) node.
956    pub bound_node_var: String,
957    /// Direction of the extend (forward, backward, or both).
958    pub direction: akar_parser::ast::EdgeDirection,
959    /// Variable name of the destination node (e.g., "p").
960    pub dst_node_var: String,
961    /// Table name of the destination node (e.g., "Post").
962    pub dst_table_name: String,
963    /// Table ID of the destination node.
964    pub dst_table_id: u64,
965    /// Estimated cardinality.
966    pub cardinality: u64,
967}
968
969#[derive(Debug, Clone)]
970pub struct LogicalCreateRel {
971    pub table_name: String,
972    pub table_id: u64,
973    pub src_node_name: String,
974    pub dst_node_name: String,
975    pub out_var_name: String,
976    pub properties: Vec<(String, akar_parser::ast::Expression)>,
977    pub cardinality: u64,
978}
979
980/// Logical operator for EXPORT DATABASE.
981#[derive(Debug, Clone)]
982pub struct LogicalExportDatabase {
983    pub file_path: String,
984    pub file_type: String,
985    pub schema_only: bool,
986    pub options: std::collections::HashMap<String, String>,
987    pub cardinality: u64,
988}
989
990/// Logical operator for IMPORT DATABASE.
991#[derive(Debug, Clone)]
992pub struct LogicalImportDatabase {
993    pub file_path: String,
994    pub query: String,
995    pub index_query: String,
996    pub cardinality: u64,
997}
998
999/// Logical operator for creating an FTS index (P8 architecture)
1000#[derive(Debug, Clone)]
1001pub struct LogicalCreateFtsIndex {
1002    pub index_name: String,
1003    pub table_name: String,
1004    pub column_name: String,
1005    pub if_not_exists: bool,
1006    /// Derived macro table names.
1007    pub docs_table: String,
1008    pub terms_table: String,
1009    pub posting_table: String,
1010    pub cardinality: u64,
1011}
1012
1013/// Logical operator for querying an FTS index via `USING FTS INDEX` clause.
1014#[derive(Debug, Clone)]
1015pub struct LogicalFtsScan {
1016    pub index_name: String,
1017    pub query_string: String,
1018    pub docs_table: String,
1019    pub terms_table: String,
1020    pub posting_table: String,
1021    /// Source node table/column the index was created on (P52.39).
1022    pub table_name: String,
1023    pub column_name: String,
1024    pub cardinality: u64,
1025}
1026
1027/// EmptyResult operator — returns an empty result set (0 rows).
1028/// Inserted when planner knows the query will yield no rows (e.g. WHERE false).
1029#[derive(Debug, Clone)]
1030pub struct LogicalEmptyResult {
1031    pub cardinality: u64,
1032}
1033
1034/// MultiplicityReducer operator — deduplicates rows from pattern matching fan-out.
1035#[derive(Debug, Clone)]
1036pub struct LogicalMultiplicityReducer {
1037    pub key_columns: Vec<usize>,
1038    pub children: Vec<LogicalOperator>,
1039    pub cardinality: u64,
1040}
1041
1042/// Skip operator — skips the first N rows (like LIMIT offset without limit).
1043#[derive(Debug, Clone)]
1044pub struct LogicalSkip {
1045    pub offset: u64,
1046    pub children: Vec<LogicalOperator>,
1047    pub cardinality: u64,
1048}
1049
1050/// Insert operator — row-level insertion (unlike BatchInsert).
1051#[derive(Debug, Clone)]
1052pub struct LogicalInsert {
1053    pub table_name: String,
1054    pub table_id: u64,
1055    pub columns: Vec<String>,
1056    pub values: Vec<Vec<akar_common::types::Value>>,
1057    pub cardinality: u64,
1058}
1059
1060/// ExtensionClause operator — handles EXTENSION commands (INSTALL, LOAD).
1061#[derive(Debug, Clone)]
1062pub struct LogicalExtensionClause {
1063    pub action: akar_parser::ast::ExtensionAction,
1064    pub extension_name: String,
1065    pub cardinality: u64,
1066}