xlog_ir/rir.rs
1//! Relational IR node definitions
2
3use xlog_core::{AggOp, RelId, ScalarType};
4
5/// Join type variants
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum JoinType {
8 /// Standard inner join
9 Inner,
10 /// Left outer join
11 LeftOuter,
12 /// Semi join (exists check)
13 Semi,
14 /// Anti join (not exists / negation)
15 Anti,
16}
17
18/// Expression in filter predicates
19#[derive(Debug, Clone, PartialEq)]
20pub enum Expr {
21 /// Column reference by index
22 Column(usize),
23 /// Constant value
24 Const(ConstValue),
25 /// Binary comparison
26 Compare {
27 /// Left-hand side expression.
28 left: Box<Expr>,
29 /// Comparison operator.
30 op: CompareOp,
31 /// Right-hand side expression.
32 right: Box<Expr>,
33 },
34 /// Logical AND
35 And(Vec<Expr>),
36 /// Logical OR
37 Or(Vec<Expr>),
38 /// Logical NOT
39 Not(Box<Expr>),
40
41 // Arithmetic operations
42 /// Addition
43 Add(Box<Expr>, Box<Expr>),
44 /// Subtraction
45 Sub(Box<Expr>, Box<Expr>),
46 /// Multiplication
47 Mul(Box<Expr>, Box<Expr>),
48 /// Division
49 Div(Box<Expr>, Box<Expr>),
50 /// Modulo
51 Mod(Box<Expr>, Box<Expr>),
52
53 // Built-in functions
54 /// Absolute value
55 Abs(Box<Expr>),
56 /// Minimum of two values
57 Min(Box<Expr>, Box<Expr>),
58 /// Maximum of two values
59 Max(Box<Expr>, Box<Expr>),
60 /// Power (base, exponent)
61 Pow(Box<Expr>, Box<Expr>),
62 /// Type cast
63 Cast(Box<Expr>, ScalarType),
64
65 /// Conditional expression: if condition then then_expr else else_expr
66 /// The condition is a boolean comparison expression.
67 /// Used for UDF conditionals like: if X >= 100 then 1 else 2
68 Conditional {
69 /// Boolean condition (should evaluate to bool)
70 condition: Box<Expr>,
71 /// Expression to evaluate when condition is true
72 then_expr: Box<Expr>,
73 /// Expression to evaluate when condition is false
74 else_expr: Box<Expr>,
75 },
76}
77
78/// Projection expression -- either a pass-through column reference or a computed value.
79#[derive(Debug, Clone, PartialEq)]
80pub enum ProjectExpr {
81 /// Pass through column at given index.
82 Column(usize),
83 /// Compute an expression whose result has the given scalar type.
84 Computed(Expr, ScalarType),
85}
86
87/// Per-lookup-input permutation for adaptive variable ordering.
88///
89/// When a non-default leader is chosen, the dispatcher rotates kernel
90/// inputs and may swap the two columns of selected lookup atoms (triangle
91/// only — the 4-cycle has rotational symmetry and never needs col-swap).
92/// `swap_cols == true` means the dispatcher must materialize an owned
93/// 2-col view with cols swapped before calling the layout helper.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct LookupPerm {
96 /// Index into the **promoter's canonical input order**:
97 /// triangle = `[e_xy, e_yz, e_xz]`, 4-cycle =
98 /// `[e_wx, e_xy, e_yz, e_zw]`. `lookup_perms[i]` describes
99 /// kernel slot `i + 1` (slots 1, 2, 3 — the non-leader slots).
100 /// The leader slot 0 is identified by `VariableOrder::leader_idx`
101 /// and is never repeated here.
102 pub input_idx: u8,
103 /// Whether to swap col0 ↔ col1 on this input before the layout
104 /// helper sees it.
105 pub swap_cols: bool,
106}
107
108/// Maximum K supported by the K-clique variable-order plan.
109pub const K_CLIQUE_MAX_K: usize = 8;
110
111/// Maximum edge count for K=8 complete binary-edge clique, C(8, 2).
112pub const K_CLIQUE_MAX_EDGES: usize = 28;
113
114/// Column-order rewrite for one K-clique input edge.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct ColumnSwap {
117 /// Edge slot to rewrite after edge permutation.
118 pub edge_slot: u8,
119 /// Whether the two source columns should be swapped.
120 pub swap_cols: bool,
121}
122
123/// Sorted-layout requirements carried by a K-clique plan.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct SortedLayoutSpec {
126 /// Edge slots whose sorted layouts are required by the plan.
127 pub edge_slots: Vec<u8>,
128 /// Per-edge key-column order required by the sorted layout.
129 pub key_columns: Vec<Vec<u8>>,
130}
131
132/// Helper relation split requested by the K-clique plan.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct HelperSplitSpec {
135 /// Stable helper identifier within the plan.
136 pub helper_id: u8,
137 /// Variable whose prefix/fanout is split into the helper.
138 pub variable: u8,
139 /// Edge slots materialized into the helper relation.
140 pub edge_slots: Vec<u8>,
141}
142
143/// Stream group assigned to a K-clique plan.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
145pub struct StreamGroupId(pub u8);
146
147/// Full variable-order plan for K=5..K=8 clique-family WCOJ dispatch.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct KCliqueVariableOrder {
150 /// Clique arity K.
151 pub k: u8,
152 /// Position for each variable id; unused entries are `u8::MAX`.
153 pub variable_positions: [u8; K_CLIQUE_MAX_K],
154 /// Edge-slot permutation; unused entries are `u8::MAX`.
155 pub edge_permutation: [u8; K_CLIQUE_MAX_EDGES],
156 /// Optional column swaps after edge permutation.
157 pub column_swaps: Vec<ColumnSwap>,
158 /// Sorted-layout requirements for runtime layout construction.
159 pub sorted_layout_requirements: SortedLayoutSpec,
160 /// Helper-split requests attached to this plan.
161 pub helper_split_specs: Vec<HelperSplitSpec>,
162 /// Stream group consumed by stream-mux scheduling.
163 pub stream_group: StreamGroupId,
164}
165
166impl KCliqueVariableOrder {
167 /// Creates a K-clique variable-order plan with all seven required fields.
168 pub fn new(
169 k: u8,
170 variable_positions: [u8; K_CLIQUE_MAX_K],
171 edge_permutation: [u8; K_CLIQUE_MAX_EDGES],
172 column_swaps: Vec<ColumnSwap>,
173 sorted_layout_requirements: SortedLayoutSpec,
174 helper_split_specs: Vec<HelperSplitSpec>,
175 stream_group: StreamGroupId,
176 ) -> Self {
177 Self {
178 k,
179 variable_positions,
180 edge_permutation,
181 column_swaps,
182 sorted_layout_requirements,
183 helper_split_specs,
184 stream_group,
185 }
186 }
187}
188
189/// Cost evidence carried with a planned WCOJ-vs-hash route.
190#[derive(Debug, Clone, Copy, PartialEq)]
191pub struct CostPredictionRecord {
192 /// Estimated WCOJ work under the selected plan.
193 pub wcoj_cost: f64,
194 /// Estimated hash-chain work under the captured fallback plan.
195 pub hash_cost: f64,
196}
197
198impl CostPredictionRecord {
199 /// Stable evidence for incomplete stats: hash is the safe default route.
200 pub fn empty() -> Self {
201 Self {
202 wcoj_cost: f64::INFINITY,
203 hash_cost: 0.0,
204 }
205 }
206}
207
208/// Auditable reason for a structured hash route.
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub enum PlannedHashReason {
211 /// Planner had complete stats and predicted hash lower-cost.
212 PlannerPredictsHashWins,
213 /// Planner could not build a complete stats-backed plan.
214 IncompleteStatsSafeDefault,
215}
216
217/// Route chosen for a recognized multiway shape.
218#[derive(Debug, Clone, PartialEq)]
219pub enum MultiwayPlan {
220 /// Execute the WCOJ path with the attached K-clique plan.
221 WcojWithPlan(KCliqueVariableOrder),
222 /// Execute the captured fallback as a planned hash route.
223 PlannedHashRoute {
224 /// Why the recognized shape routes to hash.
225 reason: PlannedHashReason,
226 /// Cost evidence that made the route auditable.
227 planner_evidence: CostPredictionRecord,
228 },
229 /// Generic Free Join route emitted ONLY by the general
230 /// multiway promoter. Provenance contract: `inputs` are the
231 /// fallback body's Scan leaves in left-to-right traversal order,
232 /// so `output_columns` (which carries the fallback projection, as
233 /// on every `MultiWayJoin`) coincides positionally with the
234 /// concatenated-inputs column space — the property the Free Join
235 /// dispatcher's head projection relies on. Dedicated-shape
236 /// promoters reorder `inputs` canonically and must never use this
237 /// variant; the dispatcher declines every other plan value.
238 FreeJoin,
239}
240
241/// Variable-ordering decision attached to a `MultiWayJoin`.
242///
243/// `None` on the parent variant preserves legacy triangle, 4-cycle, and
244/// recursive dispatch behavior bit-identically (default leader, no col-swap,
245/// no kernel projection — `output_columns` carries the binary-fallback
246/// projection as before).
247///
248/// When `Some`, the dispatcher consumes `leader_idx` to rotate the
249/// kernel `inputs`, applies any `lookup_perms` col-swaps, and
250/// post-projects the kernel-direct output buffer through
251/// `kernel_output_cols`. `MultiWayJoin::output_columns` stays untouched
252/// so binary-fallback consumers continue reading it directly.
253#[derive(Debug, Clone, PartialEq)]
254pub struct VariableOrder {
255 /// Selected leader's index in the canonical promoter input order
256 /// (e.g., for triangle: 0=e_xy, 1=e_yz, 2=e_xz). `0` reproduces
257 /// the default leader.
258 pub leader_idx: u8,
259 /// One entry per non-leader lookup input, in dispatcher slot order.
260 pub lookup_perms: Vec<LookupPerm>,
261 /// Permutation applied to the kernel-direct output buffer to
262 /// produce head-ordered columns. For default leader this would be
263 /// identity but the field is omitted (`var_order = None`) — the legacy
264 /// triangle/4-cycle path keeps using `MultiWayJoin::output_columns`
265 /// directly.
266 pub kernel_output_cols: Vec<ProjectExpr>,
267 /// Full K-clique variable-order plan for K=5..K=8. `None`
268 /// preserves the legacy triangle/4-cycle leader-permutation path.
269 pub kclique: Option<KCliqueVariableOrder>,
270}
271
272impl VariableOrder {
273 /// Creates the legacy triangle/4-cycle leader-permutation form.
274 pub fn legacy(
275 leader_idx: u8,
276 lookup_perms: Vec<LookupPerm>,
277 kernel_output_cols: Vec<ProjectExpr>,
278 ) -> Self {
279 Self {
280 leader_idx,
281 lookup_perms,
282 kernel_output_cols,
283 kclique: None,
284 }
285 }
286
287 /// Creates the full K-clique variable-order form.
288 pub fn kclique(kclique: KCliqueVariableOrder) -> Self {
289 Self {
290 leader_idx: 0,
291 lookup_perms: Vec::new(),
292 kernel_output_cols: Vec::new(),
293 kclique: Some(kclique),
294 }
295 }
296}
297
298/// Comparison operators
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum CompareOp {
301 /// Equal (`==`)
302 Eq,
303 /// Not equal (`!=`)
304 Ne,
305 /// Less than (`<`)
306 Lt,
307 /// Less than or equal (`<=`)
308 Le,
309 /// Greater than (`>`)
310 Gt,
311 /// Greater than or equal (`>=`)
312 Ge,
313}
314
315/// Constant values in expressions
316#[derive(Debug, Clone, PartialEq)]
317pub enum ConstValue {
318 /// Unsigned 32-bit integer constant.
319 U32(u32),
320 /// Unsigned 64-bit integer constant.
321 U64(u64),
322 /// Signed 32-bit integer constant.
323 I32(i32),
324 /// Signed 64-bit integer constant.
325 I64(i64),
326 /// 32-bit float constant.
327 F32(f32),
328 /// 64-bit float constant.
329 F64(f64),
330 /// Boolean constant.
331 Bool(bool),
332 /// Interned symbol string constant.
333 Symbol(String),
334}
335
336/// Relational IR node types
337#[derive(Debug, Clone)]
338#[allow(clippy::large_enum_variant)]
339pub enum RirNode {
340 /// A 0-arity relation containing exactly one empty tuple ({()}).
341 ///
342 /// This is the identity element for joins and the natural seed for rules whose bodies
343 /// contain no positive atoms (e.g. `p() :- not q().`), allowing negation-only rules to
344 /// be lowered as set difference against a unit relation.
345 Unit,
346
347 /// Scan a base relation
348 Scan {
349 /// Relation identifier to scan.
350 rel: RelId,
351 },
352
353 /// Filter rows by predicate
354 Filter {
355 /// Input relation subtree to filter.
356 input: Box<RirNode>,
357 /// Boolean predicate applied to each row.
358 predicate: Expr,
359 },
360
361 /// Project specific columns (pass-through or computed)
362 Project {
363 /// Input relation subtree to project from.
364 input: Box<RirNode>,
365 /// Output projection expressions in result-column order.
366 columns: Vec<ProjectExpr>,
367 },
368
369 /// Join two relations
370 Join {
371 /// Left-hand input relation.
372 left: Box<RirNode>,
373 /// Right-hand input relation.
374 right: Box<RirNode>,
375 /// Column indices from the left input used as join keys.
376 left_keys: Vec<usize>,
377 /// Column indices from the right input used as join keys.
378 right_keys: Vec<usize>,
379 /// Join semantics to apply.
380 join_type: JoinType,
381 },
382
383 /// Production two-atom chain join:
384 /// `head(...) :- left(..., Z, ...), right(..., Z, ...)`.
385 ///
386 /// The executor MAY dispatch this node through a specialized
387 /// physical route. On dispatch decline, it must execute `fallback`,
388 /// the IR-equivalent binary join captured at promotion time.
389 ChainJoin {
390 /// Left relation input. The chain-join promoter emits a Scan.
391 left: Box<RirNode>,
392 /// Right relation input. The chain-join promoter emits a Scan.
393 right: Box<RirNode>,
394 /// Join key column in `left`.
395 left_key: usize,
396 /// Join key column in `right`.
397 right_key: usize,
398 /// Output projection in head-tuple order.
399 output_columns: Vec<ProjectExpr>,
400 /// IR-equivalent binary-join plan for fallback execution.
401 fallback: Box<RirNode>,
402 },
403
404 /// Group by with aggregation
405 GroupBy {
406 /// Input relation subtree to aggregate.
407 input: Box<RirNode>,
408 /// Column indices preserved as grouping keys.
409 key_cols: Vec<usize>,
410 /// (value_column, aggregation_op)
411 aggs: Vec<(usize, AggOp)>,
412 },
413
414 /// Union multiple inputs
415 Union {
416 /// Input subtrees whose rows are concatenated together.
417 inputs: Vec<RirNode>,
418 },
419
420 /// Remove duplicates
421 Distinct {
422 /// Input relation subtree to deduplicate.
423 input: Box<RirNode>,
424 /// Column indices defining tuple identity.
425 key_cols: Vec<usize>,
426 },
427
428 /// Set difference (left - right)
429 Diff {
430 /// Left-hand input relation.
431 left: Box<RirNode>,
432 /// Right-hand input relation whose rows are excluded from the left input.
433 right: Box<RirNode>,
434 },
435
436 /// Fixpoint iteration for recursion
437 Fixpoint {
438 /// SCC identifier
439 scc_id: u32,
440 /// Base case computation
441 base: Box<RirNode>,
442 /// Recursive step computation
443 recursive: Box<RirNode>,
444 /// Relation for delta (new tuples)
445 delta_rel: RelId,
446 /// Relation for full result
447 full_rel: RelId,
448 },
449
450 /// A multi-way conjunctive join that the executor MAY dispatch to a
451 /// specialized physical operator (e.g. GPU WCOJ). When the dispatch
452 /// declines, the executor falls through to `fallback`, which is the
453 /// IR-equivalent binary-join plan captured at promotion time.
454 ///
455 /// **Invariant** (upheld by `xlog-logic::promote::promote_multiway`):
456 /// executing `fallback` produces the same row set as a successful
457 /// specialized dispatch.
458 ///
459 /// The original promoter emitted this for the triangle shape; later
460 /// promoters also use it for 4-cycle and general-arity joins.
461 ///
462 /// # Walker contract
463 ///
464 /// Generic walkers and visitors that handle `MultiWayJoin` MUST be
465 /// shape-agnostic over `inputs`, `slot_vars`, and `output_columns`
466 /// — no walker may assume a fixed arity or a specific
467 /// variable-class layout. Only matchers/promoters whose name
468 /// carries an explicit shape qualifier (e.g.
469 /// `match_multiway_triangle`, `try_promote_triangle`) may lock to
470 /// a specific shape.
471 MultiWayJoin {
472 /// Input scans, in physical-plan slot order. For the original
473 /// triangle promoter, this is exactly `[Scan(rel_xy), Scan(rel_yz),
474 /// Scan(rel_xz)]` for a recognized triangle. Each input MUST be
475 /// `RirNode::Scan { rel }`.
476 inputs: Vec<RirNode>,
477 /// Per-slot, per-column variable-class id. Same id across slots →
478 /// join on that variable. For the canonical triangle this is
479 /// `[[Some(0), Some(1)], [Some(1), Some(2)], [Some(0), Some(2)]]`.
480 /// `None` is reserved for constant-bound or don't-care columns;
481 /// the v1 promoter never emits `None`.
482 slot_vars: Vec<Vec<Option<u32>>>,
483 /// Output projection in head-tuple order, identical to what the
484 /// equivalent `Project { input: Join { ... } }` carries. For the
485 /// triangle: `[Column(0), Column(1), Column(3)]`. The executor
486 /// re-validates this; a malformed or rotated projection is
487 /// treated as ineligible (no dispatch).
488 output_columns: Vec<ProjectExpr>,
489 /// IR-equivalent binary-join plan. Executed verbatim on dispatch
490 /// decline. Captured from the post-optimizer tree by the
491 /// promoter; never synthesized.
492 fallback: Box<RirNode>,
493 /// Structured route for recognized multiway shapes. K-clique
494 /// cost-gated hash routes are positive plans, not promoter
495 /// inability to handle the shape.
496 plan: Option<MultiwayPlan>,
497 /// Optional adaptive variable-ordering decision.
498 ///
499 /// `None` preserves legacy triangle, 4-cycle, and recursive dispatch
500 /// behavior bit-identically: dispatcher uses default leader, no
501 /// col-swap, post-kernel projection is the existing `output_columns`.
502 ///
503 /// `Some(VariableOrder)` instructs the dispatcher to rotate
504 /// kernel inputs to put `leader_idx` at slot 0, apply
505 /// `lookup_perms` col-swaps, and post-project via
506 /// `kernel_output_cols`. `output_columns` is NOT consulted on
507 /// the adaptive variable-ordering path; binary-fallback consumers
508 /// still read it.
509 var_order: Option<VariableOrder>,
510 },
511
512 /// Tensorized ILP super-graph join. A DLPack mask tensor selects which
513 /// (body_i, body_j) → head_k rule combinations are active.
514 TensorMaskedJoin {
515 /// Name of the mask tensor registered in the runtime.
516 mask_name: String,
517 /// Arity of the relation schema participating in the tensorized join.
518 schema_size: usize,
519 /// Left-side join key columns within the body schema.
520 left_keys: Vec<usize>,
521 /// Right-side join key columns within the body schema.
522 right_keys: Vec<usize>,
523 /// Mapping from tensor dimension index → (RelId, relation name).
524 /// Sorted by RelId for deterministic ordering.
525 rel_index: Vec<(RelId, String)>,
526 /// Head relation name for store lookup in the executor.
527 head_rel_name: String,
528 /// Head relation ID for optimizer schema lookup, keyed by RelId.
529 head_rel_id: RelId,
530 /// Maximum active rules to process as a budget cap.
531 max_active_rules: usize,
532 /// Column indices from the join result to project into the head schema.
533 /// Maps head column `i` to join result column `head_projection[i]`.
534 /// Join result columns are: [left_col_0..left_col_n, right_col_0..right_col_m].
535 head_projection: Vec<usize>,
536 },
537}
538
539impl RirNode {
540 /// Check if this node is a leaf (Scan)
541 pub fn is_leaf(&self) -> bool {
542 matches!(self, RirNode::Scan { .. })
543 }
544
545 /// Get all relation IDs referenced in this subtree
546 pub fn referenced_relations(&self) -> Vec<RelId> {
547 let mut rels = Vec::new();
548 self.collect_relations(&mut rels);
549 rels
550 }
551
552 fn collect_relations(&self, rels: &mut Vec<RelId>) {
553 match self {
554 RirNode::Unit => {}
555 RirNode::Scan { rel } => rels.push(*rel),
556 RirNode::Filter { input, .. } | RirNode::Project { input, .. } => {
557 input.collect_relations(rels);
558 }
559 RirNode::Join { left, right, .. }
560 | RirNode::ChainJoin { left, right, .. }
561 | RirNode::Diff { left, right } => {
562 left.collect_relations(rels);
563 right.collect_relations(rels);
564 }
565 RirNode::Union { inputs } => {
566 for input in inputs {
567 input.collect_relations(rels);
568 }
569 }
570 RirNode::GroupBy { input, .. } | RirNode::Distinct { input, .. } => {
571 input.collect_relations(rels);
572 }
573 RirNode::Fixpoint {
574 base,
575 recursive,
576 delta_rel,
577 full_rel,
578 ..
579 } => {
580 base.collect_relations(rels);
581 recursive.collect_relations(rels);
582 rels.push(*delta_rel);
583 rels.push(*full_rel);
584 }
585 RirNode::TensorMaskedJoin { rel_index, .. } => {
586 for (rel_id, _) in rel_index {
587 rels.push(*rel_id);
588 }
589 }
590 RirNode::MultiWayJoin { inputs, .. } => {
591 // Recurse into `inputs` only. The `fallback` references
592 // the same set by promoter invariant; walking both would
593 // double-count.
594 for input in inputs {
595 input.collect_relations(rels);
596 }
597 }
598 }
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use xlog_core::ScalarType;
606
607 #[test]
608 fn test_scan_node() {
609 let node = RirNode::Scan { rel: RelId(1) };
610 assert!(matches!(node, RirNode::Scan { rel: RelId(1) }));
611 assert!(node.is_leaf());
612 }
613
614 #[test]
615 fn test_join_node() {
616 let left = Box::new(RirNode::Scan { rel: RelId(1) });
617 let right = Box::new(RirNode::Scan { rel: RelId(2) });
618 let join = RirNode::Join {
619 left,
620 right,
621 left_keys: vec![0],
622 right_keys: vec![0],
623 join_type: JoinType::Inner,
624 };
625 assert!(matches!(join, RirNode::Join { .. }));
626 let rels = join.referenced_relations();
627 assert!(rels.contains(&RelId(1)));
628 assert!(rels.contains(&RelId(2)));
629 }
630
631 #[test]
632 fn test_fixpoint_node() {
633 let base = Box::new(RirNode::Scan { rel: RelId(1) });
634 let recursive = Box::new(RirNode::Scan { rel: RelId(2) });
635 let fp = RirNode::Fixpoint {
636 scc_id: 0,
637 base,
638 recursive,
639 delta_rel: RelId(3),
640 full_rel: RelId(4),
641 };
642 assert!(matches!(fp, RirNode::Fixpoint { scc_id: 0, .. }));
643 }
644
645 #[test]
646 fn test_anti_join() {
647 let left = Box::new(RirNode::Scan { rel: RelId(1) });
648 let right = Box::new(RirNode::Scan { rel: RelId(2) });
649 let anti = RirNode::Join {
650 left,
651 right,
652 left_keys: vec![0],
653 right_keys: vec![0],
654 join_type: JoinType::Anti,
655 };
656 if let RirNode::Join { join_type, .. } = anti {
657 assert_eq!(join_type, JoinType::Anti);
658 }
659 }
660
661 #[test]
662 fn test_expr_arithmetic() {
663 let expr = Expr::Add(
664 Box::new(Expr::Column(0)),
665 Box::new(Expr::Const(ConstValue::I64(1))),
666 );
667 assert!(matches!(expr, Expr::Add(_, _)));
668 }
669
670 #[test]
671 fn test_project_expr_computed() {
672 let proj = ProjectExpr::Computed(
673 Expr::Add(
674 Box::new(Expr::Column(0)),
675 Box::new(Expr::Const(ConstValue::I64(1))),
676 ),
677 ScalarType::I64,
678 );
679 assert!(matches!(proj, ProjectExpr::Computed(_, _)));
680 }
681}