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 index_name: String,
418    pub index_id: u64,
419    pub query_vector: Vec<f64>,
420    pub top_k: u64,
421    pub table_name: String,
422    pub cardinality: u64,
423}
424
425#[derive(Debug, Clone)]
426pub struct LogicalScanNode {
427    pub table_name: String,
428    pub table_id: u64,
429    pub alias: Option<String>,
430    pub columns: Vec<String>,
431    pub cardinality: u64,
432    pub fts_query: Option<LogicalFtsScan>,
433    pub predicate: Option<Expression>,
434}
435
436#[derive(Debug, Clone)]
437pub struct LogicalScanRel {
438    pub table_name: String,
439    pub table_id: u64,
440    pub direction: akar_parser::ast::EdgeDirection,
441    pub cardinality: u64,
442}
443
444#[derive(Debug, Clone)]
445pub struct LogicalFilter {
446    pub expression: Expression,
447    pub children: Vec<LogicalOperator>,
448    pub cardinality: u64,
449}
450
451#[derive(Debug, Clone)]
452pub struct LogicalProjection {
453    pub expressions: Vec<BoundExpression>,
454    pub children: Vec<LogicalOperator>,
455    pub cardinality: u64,
456}
457
458#[derive(Debug, Clone)]
459pub struct LogicalHashJoin {
460    pub join_keys: Vec<Expression>,
461    pub build_side: Box<LogicalOperator>,
462    pub probe_side: Box<LogicalOperator>,
463    pub cardinality: u64,
464    /// Whether this join is eligible for foreign join push-down optimization.
465    /// Set by the ForeignJoinPushDown optimizer pass when all tables in the
466    /// pattern belong to the same foreign database.
467    pub push_down_eligible: bool,
468}
469
470/// Semi-join: returns left rows that have a matching key in the right side.
471/// Like HashJoin but only emits left columns for matching rows.
472#[derive(Debug, Clone)]
473pub struct LogicalSemiJoin {
474    pub join_keys: Vec<Expression>,
475    pub left: Box<LogicalOperator>,
476    pub right: Box<LogicalOperator>,
477    pub cardinality: u64,
478}
479
480/// Anti-join: returns left rows that have NO matching key in the right side.
481/// Like SemiJoin but inverts the match condition.
482#[derive(Debug, Clone)]
483pub struct LogicalAntiJoin {
484    pub join_keys: Vec<Expression>,
485    pub left: Box<LogicalOperator>,
486    pub right: Box<LogicalOperator>,
487    pub cardinality: u64,
488}
489
490/// EXPLAIN operator — wraps a child plan and produces a textual plan description.
491///
492/// Unlike other operators, Explain does not execute its child; instead it
493/// serializes the operator tree to a human-readable string.
494#[derive(Debug, Clone)]
495pub struct LogicalExplain {
496    /// The inner operator tree to explain.
497    pub inner: Box<LogicalOperator>,
498    /// The type of explain output.
499    pub explain_type: akar_parser::ast::ExplainType,
500    /// Cardinality (always 1 — one row with the plan string).
501    pub cardinality: u64,
502}
503
504#[derive(Debug, Clone)]
505pub struct LogicalCrossProduct {
506    pub left: Box<LogicalOperator>,
507    pub right: Box<LogicalOperator>,
508    pub cardinality: u64,
509}
510
511#[derive(Debug, Clone)]
512pub struct LogicalOrderBy {
513    pub sort_keys: Vec<(Expression, bool)>, // (expression, ascending)
514    pub children: Vec<LogicalOperator>,
515    pub cardinality: u64,
516}
517
518/// A fused ORDER BY + LIMIT operator for Top-K optimization.
519///
520/// When the optimizer detects a consecutive ORDER BY followed by LIMIT,
521/// it fuses them into a single LogicalTopK. This signals the processor
522/// to use a BinaryHeap-based TopK execution (O(n log k)) instead of
523/// full sort + limit (O(n log n)).
524#[derive(Debug, Clone)]
525pub struct LogicalTopK {
526    pub sort_keys: Vec<(Expression, bool)>,
527    pub limit: u64,
528    pub offset: u64,
529    pub children: Vec<LogicalOperator>,
530    pub cardinality: u64,
531}
532
533#[derive(Debug, Clone)]
534pub struct LogicalLimit {
535    pub limit: u64,
536    pub offset: u64,
537    pub children: Vec<LogicalOperator>,
538    pub cardinality: u64,
539}
540
541#[derive(Debug, Clone)]
542pub struct LogicalAggregate {
543    pub group_by: Vec<Expression>,
544    pub aggregates: Vec<(String, Vec<Expression>)>, // (function_name, args)
545    pub children: Vec<LogicalOperator>,
546    pub cardinality: u64,
547}
548
549#[derive(Debug, Clone)]
550pub struct LogicalUnion {
551    pub left: Box<LogicalOperator>,
552    pub right: Box<LogicalOperator>,
553    pub all: bool,
554    pub cardinality: u64,
555}
556
557/// A flatten operator that converts a specific factorization group from
558/// unflat (list-like) to flat (scalar) representation.
559///
560/// Inserted by `FactorizationRewriting` to ensure operators like HashJoin
561/// receive the correct factorization layout.
562#[derive(Debug, Clone)]
563pub struct LogicalFlatten {
564    pub group_pos: usize,
565    pub children: Vec<LogicalOperator>,
566    pub cardinality: u64,
567}
568
569/// UNWIND operator — expands a list expression into rows.
570#[derive(Debug, Clone)]
571pub struct LogicalUnwind {
572    pub expression: akar_parser::ast::Expression,
573    pub variable: String,
574    pub cardinality: u64,
575}
576
577/// INTERSECT operator — finds common keys across multiple build sides.
578///
579/// Used for multi-pattern matching like `MATCH (a)-[:r1]->(b), (a)-[:r2]->(c)`
580/// where `a` is the shared key. Intersect probes multiple build hash tables
581/// and outputs combined payloads only for keys present in all build sides.
582#[derive(Debug, Clone)]
583pub struct LogicalIntersect {
584    /// Number of build sides (hash tables to probe).
585    pub num_build_sides: u32,
586    /// Key expressions for each build side.
587    pub build_key_exprs: Vec<Expression>,
588    /// Probe side — produces the shared key value to look up.
589    pub left: Box<LogicalOperator>,
590    /// Build side — produces hash tables for each pattern.
591    pub right: Box<LogicalOperator>,
592    /// Estimated cardinality.
593    pub cardinality: u64,
594}
595
596/// Variable-length path (recursive extend) operator.
597///
598/// Corresponds to `MATCH (a)-[e*1..3]->(b)` — traverses the graph
599/// up to `upper_bound` hops from source nodes and produces results
600/// for each path whose length is between `lower_bound` and `upper_bound`.
601///
602/// Supports both unweighted BFS and weighted shortest path (Dijkstra)
603/// when `weight_property` is specified.
604///
605/// This is a leaf operator that executes BFS/Dijkstra traversal during query execution.
606#[derive(Debug, Clone)]
607pub struct LogicalRecursiveExtend {
608    /// Source node variable name.
609    pub source_var: String,
610    /// Source node table ID.
611    pub source_table_id: u64,
612    /// Edge variable name (optional).
613    pub edge_var: Option<String>,
614    /// Destination node variable name.
615    pub target_var: String,
616    /// Relationship table ID(s) to traverse.
617    pub rel_table_ids: Vec<u64>,
618    /// Relationship label(s).
619    pub rel_labels: Vec<String>,
620    /// Minimum path length.
621    pub lower_bound: u64,
622    /// Maximum path length.
623    pub upper_bound: u64,
624    /// Traversal direction.
625    pub direction: akar_common::enums::ExtendDirection,
626    /// Path semantic (WALK / TRAIL / ACYCLIC).
627    pub semantic: akar_common::enums::PathSemantic,
628    /// Optional edge weight property name for weighted shortest path.
629    /// When `Some(prop_name)`, traversal uses Dijkstra's algorithm instead of BFS,
630    /// and results are sorted by cumulative path cost.
631    pub weight_property: Option<String>,
632    /// Optional name for the cost output column (e.g., "cost" or "totalWeight").
633    /// Only used when `weight_property` is set.
634    pub cost_output_name: Option<String>,
635    /// Estimated cardinality.
636    pub cardinality: u64,
637}
638
639/// OPTIONAL MATCH operator — a tree node with required (left) and optional (right) children.
640///
641/// The required side is executed first. For each resulting row, the optional
642/// side is attempted. If the optional side produces a match, the combined
643/// row (left + right columns) is emitted. If no match, left columns are
644/// emitted with NULLs for right-side columns.
645#[derive(Debug, Clone)]
646pub struct LogicalOptionalMatch {
647    pub left: Box<LogicalOperator>,
648    pub right: Box<LogicalOperator>,
649    pub cardinality: u64,
650}
651
652/// OPTIONAL MATCH over an already-bound pair of node variables.
653///
654/// Used when both endpoint node variables of the optional pattern are bound by
655/// the required side (e.g. `OPTIONAL MATCH (a)-[existing:Connected]-(b)` where
656/// `a` and `b` are already in scope). Unlike `LogicalOptionalMatch`, the right
657/// side is not a scan: the pattern is probed per input row against the
658/// relationship table adjacency (forward and/or reverse), emitting the edge
659/// property columns, or NULL-padding them when no edge exists (P53.25).
660#[derive(Debug, Clone)]
661pub struct LogicalOptionalExtend {
662    pub children: Vec<LogicalOperator>,
663    /// Name of the relationship table to probe.
664    pub rel_table_name: String,
665    /// ID of the relationship table.
666    pub rel_table_id: u64,
667    /// Variable name of the relationship (e.g., "existing"); prefix for the
668    /// emitted edge property columns.
669    pub rel_var: String,
670    /// Variable name of the bound source node (e.g., "a").
671    pub src_node_var: String,
672    /// Variable name of the bound destination node (e.g., "b"). An empty
673    /// string selects fan-out mode (P53.40): the destination is anonymous
674    /// (`OPTIONAL MATCH (m)-[r:R]-(:T)`), so every edge incident on the source
675    /// becomes one output row.
676    pub dst_node_var: String,
677    /// Direction of the probe (forward, backward, or both).
678    pub direction: akar_parser::ast::EdgeDirection,
679    /// Estimated cardinality.
680    pub cardinality: u64,
681}
682
683/// SET operator — updates properties on matched rows.
684#[derive(Debug, Clone)]
685pub struct LogicalSet {
686    pub table_name: String,
687    pub table_id: u64,
688    pub is_node: bool,
689    /// All assignments of the SET clause, evaluated against the pre-update
690    /// snapshot as a group (P53.17). Multiple items were previously emitted as
691    /// a chain of single-item operators, so items after the first received the
692    /// previous item's count chunk and lost the scan rows.
693    pub items: Vec<SetItem>,
694    /// True when the SET is the terminal clause (no trailing RETURN): the
695    /// operator reports "N rows updated" as column 0. False when a RETURN
696    /// follows — an empty match must flow 0 rows, not a phantom count row
697    /// (P53.39, kairos Finding P59.1).
698    pub emit_count: bool,
699    pub cardinality: u64,
700}
701
702/// A single `SET n.prop = <expr>` assignment inside a [`LogicalSet`].
703#[derive(Debug, Clone)]
704pub struct SetItem {
705    pub column_name: String,
706    pub column_idx: usize,
707    pub value: akar_parser::ast::Expression,
708}
709
710/// DELETE operator — removes rows from a table.
711#[derive(Debug, Clone)]
712pub struct LogicalDelete {
713    pub table_name: String,
714    pub table_id: u64,
715    pub primary_key_column: String,
716    pub is_node: bool,
717    pub detach: bool,
718    pub cardinality: u64,
719}
720
721/// COPY FROM operator — loads data from a file into a table.
722/// This is a leaf-level DML operator (no children) that the processor
723/// resolves into a `PhysicalCopyFrom` for execution.
724#[derive(Debug, Clone)]
725pub struct LogicalCopyFrom {
726    pub table_name: String,
727    pub table_id: u64,
728    pub file_path: String,
729    pub options: std::collections::HashMap<String, String>,
730    pub cardinality: u64,
731}
732
733/// Batch insert operator — inserts multiple rows/rels in a single operation.
734///
735/// Unlike `CopyFrom` which reads from a file, `BatchInsert` takes pre-collected
736/// data from the plan pipeline (e.g., multiple fused CREATE statements).
737/// Uses `NodeTable::insert_rows_batch()` / `RelTable::insert_rels_batch()`.
738#[derive(Debug, Clone)]
739pub struct LogicalBatchInsert {
740    pub table_name: String,
741    pub table_id: u64,
742    /// Rows to insert: each row is a Vec<Value> matching column order.
743    pub rows: Vec<Vec<akar_common::types::Value>>,
744    pub cardinality: u64,
745}
746
747/// Index lookup operator — point lookup via ART index on a PK column.
748#[derive(Debug, Clone)]
749pub struct LogicalIndexLookup {
750    pub table_name: String,
751    pub table_id: u64,
752    pub key_value: akar_common::types::Value,
753    pub cardinality: u64,
754}
755
756/// FOREACH operator — iterates over list elements and executes sub-plans.
757#[derive(Debug, Clone)]
758pub struct LogicalForeach {
759    pub variable: String,
760    pub expression: akar_parser::ast::Expression,
761    /// Sub-plans to execute for each list element.
762    pub sub_plans: Vec<Vec<LogicalOperator>>,
763    pub cardinality: u64,
764}
765
766/// A table function call operator.
767///
768/// Invokes a registered table function (e.g., `duckdb_scan`, `delta_scan`)
769/// and produces a DataChunk as output. The function is looked up by name
770/// in the FunctionRegistry during execution.
771#[derive(Debug, Clone)]
772pub struct LogicalTableFunctionCall {
773    pub function_name: String,
774    pub args: Vec<akar_parser::ast::Expression>,
775    pub cardinality: u64,
776}
777
778#[derive(Debug, Clone)]
779pub struct LogicalStandaloneCall {
780    pub function_name: String,
781    pub args: Vec<akar_parser::ast::Expression>,
782    pub cardinality: u64,
783}
784
785/// MERGE operator — match or create a node/pattern.
786///
787/// The processor first attempts to match a node with the given properties.
788/// If found, applies `ON MATCH SET` operations. If not found, creates a
789/// new node with the pattern properties and applies `ON CREATE SET`.
790#[derive(Debug, Clone)]
791pub struct LogicalMerge {
792    pub table_name: String,
793    pub table_id: u64,
794    /// Properties from the MERGE pattern (name, expression pairs).
795    pub properties: Vec<(String, akar_parser::ast::Expression)>,
796    /// SET operations to apply when the node already exists (matched).
797    pub on_match: Vec<LogicalSet>,
798    /// SET operations to apply when a new node is created.
799    pub on_create: Vec<LogicalSet>,
800    pub cardinality: u64,
801}
802
803/// Logical operator for edge MERGE (P53.20): `MERGE (a)-[r:R]->(b)`.
804///
805/// Matches an existing edge from `src` to `dst` on the rel table whose props
806/// equal the pattern properties; if absent, inserts a new edge. Emits the
807/// matched/inserted edge's `_id` as `<edge_var>._id` so a following SET clause
808/// can target it.
809#[derive(Debug, Clone)]
810pub struct LogicalMergeRel {
811    pub rel_table_name: String,
812    pub rel_table_id: u64,
813    /// Variable bound to the edge (e.g. `r`).
814    pub edge_var: String,
815    /// Node variables bound by a prior MATCH that anchor the endpoints.
816    pub src_node_var: String,
817    pub dst_node_var: String,
818    /// Inline properties from the edge pattern (`{type: $type}`).
819    pub properties: Vec<(String, akar_parser::ast::Expression)>,
820    /// SET operations applied when the edge already exists (empty for the
821    /// standalone-SET form used by Kairos `add_connection`).
822    pub on_match: Vec<LogicalSet>,
823    /// SET operations applied when a new edge is created.
824    pub on_create: Vec<LogicalSet>,
825    pub cardinality: u64,
826}
827
828// ==================== DDL Operators ====================
829
830/// Logical operator for CREATE NODE TABLE.
831#[derive(Debug, Clone)]
832pub struct LogicalCreateNodeTable {
833    pub name: String,
834    pub columns: Vec<CatalogColumn>,
835    pub primary_key: String,
836    pub cardinality: u64,
837}
838
839/// Logical operator for CREATE REL TABLE.
840#[derive(Debug, Clone)]
841pub struct LogicalCreateRelTable {
842    pub name: String,
843    pub from: String,
844    pub to: String,
845    pub columns: Vec<CatalogColumn>,
846    pub cardinality: u64,
847}
848
849/// Logical operator for DROP TABLE.
850#[derive(Debug, Clone)]
851pub struct LogicalDropTable {
852    pub name: String,
853    pub cardinality: u64,
854}
855
856/// Logical operator for ALTER TABLE.
857#[derive(Debug, Clone)]
858pub struct LogicalAlterTable {
859    pub table_name: String,
860    pub action: akar_parser::ast::AlterAction,
861    pub cardinality: u64,
862}
863
864/// Logical operator for CREATE [ART|HASH] INDEX.
865#[derive(Debug, Clone)]
866pub struct LogicalCreateIndex {
867    pub index_type: akar_catalog::IndexType,
868    pub index_name: String,
869    pub table_name: String,
870    pub column_name: String,
871    pub cardinality: u64,
872}
873
874/// Logical operator for DROP INDEX.
875#[derive(Debug, Clone)]
876pub struct LogicalDropIndex {
877    pub index_name: String,
878    pub table_name: String,
879    pub cardinality: u64,
880}
881
882/// Logical operator for CREATE VECTOR INDEX.
883#[derive(Debug, Clone)]
884pub struct LogicalCreateVectorIndex {
885    pub index_name: String,
886    pub table_name: String,
887    pub column_name: String,
888    pub metric: String,
889    pub dimensions: u64,
890    pub cardinality: u64,
891}
892
893/// Logical operator for CREATE SEQUENCE.
894#[derive(Debug, Clone)]
895pub struct LogicalCreateSequence {
896    pub name: String,
897    pub if_not_exists: bool,
898    pub or_replace: bool,
899    pub start_with: i64,
900    pub increment: i64,
901    pub min_value: i64,
902    pub max_value: i64,
903    pub cycle: bool,
904    pub cardinality: u64,
905}
906
907/// Logical operator for DROP SEQUENCE.
908#[derive(Debug, Clone)]
909pub struct LogicalDropSequence {
910    pub name: String,
911    pub if_exists: bool,
912    pub cardinality: u64,
913}
914
915/// Logical operator for CREATE DML (node creation with properties).
916#[derive(Debug, Clone)]
917pub struct LogicalCreateDml {
918    pub table_name: String,
919    pub table_id: u64,
920    pub properties: Vec<(String, akar_parser::ast::Expression)>,
921    pub cardinality: u64,
922}
923
924#[derive(Debug, Clone)]
925pub struct LogicalCreateNode {
926    pub table_name: String,
927    pub table_id: u64,
928    pub out_var_name: String,
929    pub properties: Vec<(String, akar_parser::ast::Expression)>,
930    pub cardinality: u64,
931}
932
933/// Logical operator for extending from a source node through a relationship.
934///
935/// Replaces the combination of ScanRel + ScanNode(dest) in the pipeline.
936/// For each source node, looks up adjacency list entries in the rel table,
937/// producing output rows that include source fields, rel properties, and
938/// destination node properties.
939///
940/// Ported from C++ `LogicalExtend`.
941#[derive(Debug, Clone)]
942pub struct LogicalExtend {
943    /// Name of the relationship table to extend through.
944    pub rel_table_name: String,
945    /// ID of the relationship table.
946    pub rel_table_id: u64,
947    /// Variable name of the relationship (e.g., "r" in `-[r:RELATES_TO]->`).
948    /// Used as the field-name prefix for relationship properties.
949    pub rel_var: String,
950    /// Variable name of the bound (source) node.
951    pub bound_node_var: String,
952    /// Direction of the extend (forward, backward, or both).
953    pub direction: akar_parser::ast::EdgeDirection,
954    /// Variable name of the destination node (e.g., "p").
955    pub dst_node_var: String,
956    /// Table name of the destination node (e.g., "Post").
957    pub dst_table_name: String,
958    /// Table ID of the destination node.
959    pub dst_table_id: u64,
960    /// Estimated cardinality.
961    pub cardinality: u64,
962}
963
964#[derive(Debug, Clone)]
965pub struct LogicalCreateRel {
966    pub table_name: String,
967    pub table_id: u64,
968    pub src_node_name: String,
969    pub dst_node_name: String,
970    pub properties: Vec<(String, akar_parser::ast::Expression)>,
971    pub cardinality: u64,
972}
973
974/// Logical operator for EXPORT DATABASE.
975#[derive(Debug, Clone)]
976pub struct LogicalExportDatabase {
977    pub file_path: String,
978    pub file_type: String,
979    pub schema_only: bool,
980    pub options: std::collections::HashMap<String, String>,
981    pub cardinality: u64,
982}
983
984/// Logical operator for IMPORT DATABASE.
985#[derive(Debug, Clone)]
986pub struct LogicalImportDatabase {
987    pub file_path: String,
988    pub query: String,
989    pub index_query: String,
990    pub cardinality: u64,
991}
992
993/// Logical operator for creating an FTS index (P8 architecture)
994#[derive(Debug, Clone)]
995pub struct LogicalCreateFtsIndex {
996    pub index_name: String,
997    pub table_name: String,
998    pub column_name: String,
999    pub if_not_exists: bool,
1000    /// Derived macro table names.
1001    pub docs_table: String,
1002    pub terms_table: String,
1003    pub posting_table: String,
1004    pub cardinality: u64,
1005}
1006
1007/// Logical operator for querying an FTS index via `USING FTS INDEX` clause.
1008#[derive(Debug, Clone)]
1009pub struct LogicalFtsScan {
1010    pub index_name: String,
1011    pub query_string: String,
1012    pub docs_table: String,
1013    pub terms_table: String,
1014    pub posting_table: String,
1015    /// Source node table/column the index was created on (P52.39).
1016    pub table_name: String,
1017    pub column_name: String,
1018    pub cardinality: u64,
1019}
1020
1021/// EmptyResult operator — returns an empty result set (0 rows).
1022/// Inserted when planner knows the query will yield no rows (e.g. WHERE false).
1023#[derive(Debug, Clone)]
1024pub struct LogicalEmptyResult {
1025    pub cardinality: u64,
1026}
1027
1028/// MultiplicityReducer operator — deduplicates rows from pattern matching fan-out.
1029#[derive(Debug, Clone)]
1030pub struct LogicalMultiplicityReducer {
1031    pub key_columns: Vec<usize>,
1032    pub children: Vec<LogicalOperator>,
1033    pub cardinality: u64,
1034}
1035
1036/// Skip operator — skips the first N rows (like LIMIT offset without limit).
1037#[derive(Debug, Clone)]
1038pub struct LogicalSkip {
1039    pub offset: u64,
1040    pub children: Vec<LogicalOperator>,
1041    pub cardinality: u64,
1042}
1043
1044/// Insert operator — row-level insertion (unlike BatchInsert).
1045#[derive(Debug, Clone)]
1046pub struct LogicalInsert {
1047    pub table_name: String,
1048    pub table_id: u64,
1049    pub columns: Vec<String>,
1050    pub values: Vec<Vec<akar_common::types::Value>>,
1051    pub cardinality: u64,
1052}
1053
1054/// ExtensionClause operator — handles EXTENSION commands (INSTALL, LOAD).
1055#[derive(Debug, Clone)]
1056pub struct LogicalExtensionClause {
1057    pub action: akar_parser::ast::ExtensionAction,
1058    pub extension_name: String,
1059    pub cardinality: u64,
1060}