vibesql_ast/arena/
convert.rs

1//! Conversion from arena-allocated AST types to standard AST types.
2//!
3//! This module provides conversion functions that convert arena-allocated
4//! AST nodes to their standard (heap-allocated) equivalents. This allows
5//! the arena parser to be used for performance-critical parsing while
6//! still producing standard AST types for downstream processing.
7//!
8//! # Usage
9//!
10//! ```text
11//! use bumpalo::Bump;
12//! use vibesql_ast::arena::{ArenaInterner, Converter};
13//!
14//! let arena = Bump::new();
15//! let mut interner = ArenaInterner::new(&arena);
16//! // Parse and build arena AST...
17//! let converter = Converter::new(&interner);
18//! let owned_stmt = converter.convert_select(&arena_stmt);
19//! ```
20
21use super::{
22    dml as arena_dml, expression as arena_expr,
23    interner::{ArenaInterner, Symbol},
24    select as arena_select,
25};
26use crate::{
27    Assignment, BinaryOperator, CaseWhen, CharacterUnit, CommonTableExpr, ConflictClause,
28    DeleteStmt, Expression, FrameBound, FrameUnit, FromClause, FulltextMode, FunctionIdentifier,
29    GroupByClause, GroupingElement, GroupingSet, InsertSource, InsertStmt, IntervalUnit, JoinType,
30    MixedGroupingItem, NullsOrder, OrderByItem, OrderDirection, PseudoTable, Quantifier,
31    SelectItem, SelectStmt, SetOperation, SetOperator, TrimPosition, TruthValue, UpdateStmt,
32    WhereClause, WindowFrame, WindowFunctionSpec, WindowSpec,
33};
34
35/// Converter for arena-allocated AST to owned AST.
36///
37/// This struct holds a reference to the interner used during parsing,
38/// enabling symbol resolution during conversion.
39pub struct Converter<'a, 'arena> {
40    interner: &'a ArenaInterner<'arena>,
41}
42
43impl<'a, 'arena> Converter<'a, 'arena> {
44    /// Create a new converter with the given interner.
45    pub fn new(interner: &'a ArenaInterner<'arena>) -> Self {
46        Converter { interner }
47    }
48
49    /// Resolve a symbol to its string value.
50    #[inline]
51    fn resolve(&self, sym: Symbol) -> String {
52        self.interner.resolve(sym).to_string()
53    }
54
55    /// Resolve an optional symbol.
56    #[inline]
57    fn resolve_opt(&self, sym: Option<Symbol>) -> Option<String> {
58        sym.map(|s| self.resolve(s))
59    }
60
61    /// Build a nested BinaryOp tree from a slice of arena expressions.
62    ///
63    /// Converts `[a, b, c, d]` with `And` to:
64    /// ```text
65    /// BinaryOp(And, BinaryOp(And, BinaryOp(And, a, b), c), d)
66    /// ```
67    ///
68    /// This normalization ensures compatibility with optimizer code paths
69    /// that only handle `BinaryOp::And`/`BinaryOp::Or` and not the flattened
70    /// `Conjunction`/`Disjunction` variants.
71    fn build_nested_binary_op(
72        &self,
73        children: &[arena_expr::Expression<'arena>],
74        op: BinaryOperator,
75    ) -> Expression {
76        debug_assert!(children.len() >= 2, "Conjunction/Disjunction must have at least 2 children");
77
78        let mut iter = children.iter();
79
80        // Start with the first child
81        let first = iter.next().expect("at least one child");
82        let mut result = self.convert_expression(first);
83
84        // Build left-associative tree: ((a AND b) AND c) AND d
85        for child in iter {
86            result = Expression::BinaryOp {
87                op,
88                left: Box::new(result),
89                right: Box::new(self.convert_expression(child)),
90            };
91        }
92
93        result
94    }
95
96    // ========================================================================
97    // Expression Conversion
98    // ========================================================================
99
100    /// Convert an arena Expression to an owned Expression.
101    pub fn convert_expression(&self, expr: &arena_expr::Expression<'arena>) -> Expression {
102        match expr {
103            // === Inline variants (hot path) ===
104            arena_expr::Expression::Literal(v) => Expression::Literal(v.clone()),
105            arena_expr::Expression::Placeholder(i) => Expression::Placeholder(*i),
106            arena_expr::Expression::NumberedPlaceholder(i) => Expression::NumberedPlaceholder(*i),
107            arena_expr::Expression::NamedPlaceholder(name) => {
108                Expression::NamedPlaceholder(self.resolve(*name))
109            }
110            arena_expr::Expression::ColumnRef {
111                schema,
112                table,
113                column,
114                schema_quoted,
115                table_quoted,
116                column_quoted,
117            } => {
118                let schema_str = self.resolve_opt(*schema);
119                let table_str = self.resolve_opt(*table);
120                let column_str = self.resolve(*column);
121
122                let col_id = match (schema_str, table_str) {
123                    (Some(s), Some(t)) => crate::ColumnIdentifier::fully_qualified(
124                        &s,
125                        *schema_quoted,
126                        &t,
127                        *table_quoted,
128                        &column_str,
129                        *column_quoted,
130                    ),
131                    (None, Some(t)) => crate::ColumnIdentifier::qualified(
132                        &t,
133                        *table_quoted,
134                        &column_str,
135                        *column_quoted,
136                    ),
137                    (_, None) => crate::ColumnIdentifier::simple(&column_str, *column_quoted),
138                };
139                Expression::ColumnRef(col_id)
140            }
141            arena_expr::Expression::BinaryOp { op, left, right } => Expression::BinaryOp {
142                op: *op,
143                left: Box::new(self.convert_expression(left)),
144                right: Box::new(self.convert_expression(right)),
145            },
146            arena_expr::Expression::Conjunction(children) => {
147                // Normalize flattened Conjunction to nested BinaryOp::And.
148                // This ensures all optimizer code paths work correctly,
149                // since many only handle BinaryOp::And, not Conjunction.
150                // See issue #4174 for details.
151                self.build_nested_binary_op(children, BinaryOperator::And)
152            }
153            arena_expr::Expression::Disjunction(children) => {
154                // Normalize flattened Disjunction to nested BinaryOp::Or.
155                // This ensures all optimizer code paths work correctly,
156                // since many only handle BinaryOp::Or, not Disjunction.
157                // See issue #4174 for details.
158                self.build_nested_binary_op(children, BinaryOperator::Or)
159            }
160            arena_expr::Expression::UnaryOp { op, expr } => {
161                Expression::UnaryOp { op: *op, expr: Box::new(self.convert_expression(expr)) }
162            }
163            arena_expr::Expression::IsNull { expr, negated } => Expression::IsNull {
164                expr: Box::new(self.convert_expression(expr)),
165                negated: *negated,
166            },
167            arena_expr::Expression::IsDistinctFrom { left, right, negated } => {
168                Expression::IsDistinctFrom {
169                    left: Box::new(self.convert_expression(left)),
170                    right: Box::new(self.convert_expression(right)),
171                    negated: *negated,
172                }
173            }
174            arena_expr::Expression::IsTruthValue { expr, truth_value, negated } => {
175                Expression::IsTruthValue {
176                    expr: Box::new(self.convert_expression(expr)),
177                    truth_value: (*truth_value).into(),
178                    negated: *negated,
179                }
180            }
181            arena_expr::Expression::Wildcard => Expression::Wildcard,
182            arena_expr::Expression::CurrentDate => Expression::CurrentDate,
183            arena_expr::Expression::CurrentTime { precision } => {
184                Expression::CurrentTime { precision: *precision }
185            }
186            arena_expr::Expression::CurrentTimestamp { precision } => {
187                Expression::CurrentTimestamp { precision: *precision }
188            }
189            arena_expr::Expression::Default => Expression::Default,
190
191            // === Extended variants (cold path) ===
192            arena_expr::Expression::Extended(ext) => self.convert_extended_expr(ext),
193        }
194    }
195
196    /// Convert an ExtendedExpr to an owned Expression.
197    fn convert_extended_expr(&self, ext: &arena_expr::ExtendedExpr<'arena>) -> Expression {
198        match ext {
199            arena_expr::ExtendedExpr::Function { name, args, character_unit } => {
200                Expression::Function {
201                    name: FunctionIdentifier::new(&self.resolve(*name)),
202                    args: args.iter().map(|e| self.convert_expression(e)).collect(),
203                    character_unit: character_unit.map(|u| u.into()),
204                }
205            }
206            arena_expr::ExtendedExpr::AggregateFunction { name, distinct, args } => {
207                Expression::AggregateFunction {
208                    name: FunctionIdentifier::new(&self.resolve(*name)),
209                    distinct: *distinct,
210                    args: args.iter().map(|e| self.convert_expression(e)).collect(),
211                    order_by: None, // Arena parser doesn't support ORDER BY in aggregates yet
212                    filter: None,   // Arena parser doesn't support FILTER in aggregates yet
213                }
214            }
215            arena_expr::ExtendedExpr::Case { operand, when_clauses, else_result } => {
216                Expression::Case {
217                    operand: operand.map(|e| Box::new(self.convert_expression(e))),
218                    when_clauses: when_clauses
219                        .iter()
220                        .map(|cw| self.convert_case_when(cw))
221                        .collect(),
222                    else_result: else_result.map(|e| Box::new(self.convert_expression(e))),
223                }
224            }
225            arena_expr::ExtendedExpr::ScalarSubquery(subquery) => {
226                Expression::ScalarSubquery(Box::new(self.convert_select(subquery)))
227            }
228            arena_expr::ExtendedExpr::In { expr, subquery, negated } => Expression::In {
229                expr: Box::new(self.convert_expression(expr)),
230                subquery: Box::new(self.convert_select(subquery)),
231                negated: *negated,
232            },
233            arena_expr::ExtendedExpr::InList { expr, values, negated } => Expression::InList {
234                expr: Box::new(self.convert_expression(expr)),
235                values: values.iter().map(|e| self.convert_expression(e)).collect(),
236                negated: *negated,
237            },
238            arena_expr::ExtendedExpr::Between { expr, low, high, negated, symmetric } => {
239                Expression::Between {
240                    expr: Box::new(self.convert_expression(expr)),
241                    low: Box::new(self.convert_expression(low)),
242                    high: Box::new(self.convert_expression(high)),
243                    negated: *negated,
244                    symmetric: *symmetric,
245                }
246            }
247            arena_expr::ExtendedExpr::Cast { expr, data_type } => Expression::Cast {
248                expr: Box::new(self.convert_expression(expr)),
249                data_type: data_type.clone(),
250            },
251            arena_expr::ExtendedExpr::Position { substring, string, character_unit } => {
252                Expression::Position {
253                    substring: Box::new(self.convert_expression(substring)),
254                    string: Box::new(self.convert_expression(string)),
255                    character_unit: character_unit.map(|u| u.into()),
256                }
257            }
258            arena_expr::ExtendedExpr::Trim { position, removal_char, string } => Expression::Trim {
259                position: position.map(|p| p.into()),
260                removal_char: removal_char.map(|e| Box::new(self.convert_expression(e))),
261                string: Box::new(self.convert_expression(string)),
262            },
263            arena_expr::ExtendedExpr::Extract { field, expr } => Expression::Extract {
264                field: (*field).into(),
265                expr: Box::new(self.convert_expression(expr)),
266            },
267            arena_expr::ExtendedExpr::Like { expr, pattern, negated, escape } => Expression::Like {
268                expr: Box::new(self.convert_expression(expr)),
269                pattern: Box::new(self.convert_expression(pattern)),
270                negated: *negated,
271                escape: escape.map(|e| Box::new(self.convert_expression(e))),
272            },
273            arena_expr::ExtendedExpr::Glob { expr, pattern, negated, escape } => Expression::Glob {
274                expr: Box::new(self.convert_expression(expr)),
275                pattern: Box::new(self.convert_expression(pattern)),
276                negated: *negated,
277                escape: escape.map(|e| Box::new(self.convert_expression(e))),
278            },
279            arena_expr::ExtendedExpr::Exists { subquery, negated } => Expression::Exists {
280                subquery: Box::new(self.convert_select(subquery)),
281                negated: *negated,
282            },
283            arena_expr::ExtendedExpr::QuantifiedComparison { expr, op, quantifier, subquery } => {
284                Expression::QuantifiedComparison {
285                    expr: Box::new(self.convert_expression(expr)),
286                    op: *op,
287                    quantifier: (*quantifier).into(),
288                    subquery: Box::new(self.convert_select(subquery)),
289                }
290            }
291            arena_expr::ExtendedExpr::Interval {
292                value,
293                unit,
294                leading_precision,
295                fractional_precision,
296            } => Expression::Interval {
297                value: Box::new(self.convert_expression(value)),
298                unit: (*unit).into(),
299                leading_precision: *leading_precision,
300                fractional_precision: *fractional_precision,
301            },
302            arena_expr::ExtendedExpr::DuplicateKeyValue { column } => {
303                Expression::DuplicateKeyValue { column: self.resolve(*column) }
304            }
305            arena_expr::ExtendedExpr::WindowFunction { function, over } => {
306                Expression::WindowFunction {
307                    function: self.convert_window_function_spec(function),
308                    over: self.convert_window_spec(over),
309                }
310            }
311            arena_expr::ExtendedExpr::NextValue { sequence_name } => {
312                Expression::NextValue { sequence_name: self.resolve(*sequence_name) }
313            }
314            arena_expr::ExtendedExpr::MatchAgainst { columns, search_modifier, mode } => {
315                Expression::MatchAgainst {
316                    columns: columns.iter().map(|s| self.resolve(*s)).collect(),
317                    search_modifier: Box::new(self.convert_expression(search_modifier)),
318                    mode: (*mode).into(),
319                }
320            }
321            arena_expr::ExtendedExpr::PseudoVariable { pseudo_table, column } => {
322                Expression::PseudoVariable {
323                    pseudo_table: (*pseudo_table).into(),
324                    column: self.resolve(*column),
325                }
326            }
327            arena_expr::ExtendedExpr::SessionVariable { name } => {
328                Expression::SessionVariable { name: self.resolve(*name) }
329            }
330            arena_expr::ExtendedExpr::RowValueConstructor(values) => {
331                Expression::RowValueConstructor(
332                    values.iter().map(|e| self.convert_expression(e)).collect(),
333                )
334            }
335        }
336    }
337
338    fn convert_case_when(&self, cw: &arena_expr::CaseWhen<'arena>) -> CaseWhen {
339        CaseWhen {
340            conditions: cw.conditions.iter().map(|e| self.convert_expression(e)).collect(),
341            result: self.convert_expression(&cw.result),
342        }
343    }
344
345    fn convert_window_function_spec(
346        &self,
347        spec: &arena_expr::WindowFunctionSpec<'arena>,
348    ) -> WindowFunctionSpec {
349        match spec {
350            arena_expr::WindowFunctionSpec::Aggregate { name, args } => {
351                WindowFunctionSpec::Aggregate {
352                    name: FunctionIdentifier::new(&self.resolve(*name)),
353                    args: args.iter().map(|e| self.convert_expression(e)).collect(),
354                    filter: None, // Arena parser doesn't support FILTER in window aggregates yet
355                }
356            }
357            arena_expr::WindowFunctionSpec::Ranking { name, args } => WindowFunctionSpec::Ranking {
358                name: FunctionIdentifier::new(&self.resolve(*name)),
359                args: args.iter().map(|e| self.convert_expression(e)).collect(),
360            },
361            arena_expr::WindowFunctionSpec::Value { name, args } => WindowFunctionSpec::Value {
362                name: FunctionIdentifier::new(&self.resolve(*name)),
363                args: args.iter().map(|e| self.convert_expression(e)).collect(),
364            },
365        }
366    }
367
368    fn convert_window_spec(&self, spec: &arena_expr::WindowSpec<'arena>) -> WindowSpec {
369        WindowSpec {
370            partition_by: spec
371                .partition_by
372                .as_ref()
373                .map(|v| v.iter().map(|e| self.convert_expression(e)).collect()),
374            order_by: spec
375                .order_by
376                .as_ref()
377                .map(|v| v.iter().map(|item| self.convert_order_by_item(item)).collect()),
378            frame: spec.frame.as_ref().map(|f| self.convert_window_frame(f)),
379        }
380    }
381
382    fn convert_window_frame(&self, f: &arena_expr::WindowFrame<'arena>) -> WindowFrame {
383        WindowFrame {
384            unit: f.unit.into(),
385            start: self.convert_frame_bound(&f.start),
386            end: f.end.as_ref().map(|b| self.convert_frame_bound(b)),
387            exclude: f.exclude.map(|e| match e {
388                arena_expr::FrameExclude::NoOthers => crate::FrameExclude::NoOthers,
389                arena_expr::FrameExclude::CurrentRow => crate::FrameExclude::CurrentRow,
390                arena_expr::FrameExclude::Group => crate::FrameExclude::Group,
391                arena_expr::FrameExclude::Ties => crate::FrameExclude::Ties,
392            }),
393        }
394    }
395
396    fn convert_frame_bound(&self, b: &arena_expr::FrameBound<'arena>) -> FrameBound {
397        match b {
398            arena_expr::FrameBound::UnboundedPreceding => FrameBound::UnboundedPreceding,
399            arena_expr::FrameBound::Preceding(e) => {
400                FrameBound::Preceding(Box::new(self.convert_expression(e)))
401            }
402            arena_expr::FrameBound::CurrentRow => FrameBound::CurrentRow,
403            arena_expr::FrameBound::Following(e) => {
404                FrameBound::Following(Box::new(self.convert_expression(e)))
405            }
406            arena_expr::FrameBound::UnboundedFollowing => FrameBound::UnboundedFollowing,
407        }
408    }
409
410    fn convert_order_by_item(&self, item: &arena_expr::OrderByItem<'arena>) -> OrderByItem {
411        OrderByItem {
412            expr: self.convert_expression(&item.expr),
413            direction: item.direction.into(),
414            nulls_order: item.nulls_order.map(|no| match no {
415                arena_expr::NullsOrder::First => NullsOrder::First,
416                arena_expr::NullsOrder::Last => NullsOrder::Last,
417            }),
418        }
419    }
420
421    // ========================================================================
422    // SELECT Statement Conversion
423    // ========================================================================
424
425    /// Convert an arena SelectStmt to an owned SelectStmt.
426    pub fn convert_select(&self, stmt: &arena_select::SelectStmt<'arena>) -> SelectStmt {
427        SelectStmt {
428            with_clause: stmt
429                .with_clause
430                .as_ref()
431                .map(|ctes| ctes.iter().map(|cte| self.convert_cte(cte)).collect()),
432            distinct: stmt.distinct,
433            select_list: stmt
434                .select_list
435                .iter()
436                .map(|item| self.convert_select_item(item))
437                .collect(),
438            into_table: self.resolve_opt(stmt.into_table),
439            into_variables: stmt
440                .into_variables
441                .as_ref()
442                .map(|v| v.iter().map(|s| self.resolve(*s)).collect()),
443            from: stmt.from.as_ref().map(|f| self.convert_from_clause(f)),
444            where_clause: stmt.where_clause.as_ref().map(|e| self.convert_expression(e)),
445            group_by: stmt.group_by.as_ref().map(|g| self.convert_group_by(g)),
446            having: stmt.having.as_ref().map(|e| self.convert_expression(e)),
447            order_by: stmt
448                .order_by
449                .as_ref()
450                .map(|v| v.iter().map(|item| self.convert_order_by_item(item)).collect()),
451            limit: stmt.limit.as_ref().map(|e| self.convert_expression(e)),
452            offset: stmt.offset.as_ref().map(|e| self.convert_expression(e)),
453            set_operation: stmt.set_operation.as_ref().map(|so| self.convert_set_operation(so)),
454            values: stmt.values.as_ref().map(|rows| {
455                rows.iter()
456                    .map(|row| row.iter().map(|e| self.convert_expression(e)).collect())
457                    .collect()
458            }),
459        }
460    }
461
462    fn convert_cte(&self, cte: &arena_select::CommonTableExpr<'arena>) -> CommonTableExpr {
463        CommonTableExpr {
464            name: self.resolve(cte.name),
465            columns: cte.columns.as_ref().map(|v| v.iter().map(|s| self.resolve(*s)).collect()),
466            query: Box::new(self.convert_select(cte.query)),
467            recursive: cte.recursive,
468            materialization: cte.materialization,
469        }
470    }
471
472    fn convert_select_item(&self, item: &arena_select::SelectItem<'arena>) -> SelectItem {
473        match item {
474            arena_select::SelectItem::Wildcard { alias } => SelectItem::Wildcard {
475                alias: alias.as_ref().map(|v| v.iter().map(|s| self.resolve(*s)).collect()),
476            },
477            arena_select::SelectItem::QualifiedWildcard { qualifier, alias } => {
478                SelectItem::QualifiedWildcard {
479                    qualifier: self.resolve(*qualifier),
480                    alias: alias.as_ref().map(|v| v.iter().map(|s| self.resolve(*s)).collect()),
481                }
482            }
483            arena_select::SelectItem::Expression { expr, alias, source_text } => {
484                SelectItem::Expression {
485                    expr: self.convert_expression(expr),
486                    alias: self.resolve_opt(*alias),
487                    source_text: source_text.map(|s| s.to_string()),
488                }
489            }
490        }
491    }
492
493    fn convert_from_clause(&self, from: &arena_select::FromClause<'arena>) -> FromClause {
494        match from {
495            arena_select::FromClause::Table { name, alias, column_aliases, quoted } => {
496                FromClause::Table {
497                    name: self.resolve(*name),
498                    alias: self.resolve_opt(*alias),
499                    column_aliases: column_aliases
500                        .as_ref()
501                        .map(|cols| cols.iter().map(|s| self.resolve(*s)).collect()),
502                    quoted: *quoted,
503                }
504            }
505            arena_select::FromClause::Join {
506                left,
507                right,
508                join_type,
509                condition,
510                using_columns,
511                natural,
512                alias,
513            } => FromClause::Join {
514                left: Box::new(self.convert_from_clause(left)),
515                right: Box::new(self.convert_from_clause(right)),
516                join_type: (*join_type).into(),
517                condition: condition.as_ref().map(|e| self.convert_expression(e)),
518                using_columns: using_columns
519                    .as_ref()
520                    .map(|cols| cols.iter().map(|s| self.resolve(*s)).collect()),
521                natural: *natural,
522                alias: alias.map(|s| self.resolve(s)),
523            },
524            arena_select::FromClause::Subquery { query, alias, column_aliases } => {
525                FromClause::Subquery {
526                    query: Box::new(self.convert_select(query)),
527                    alias: self.resolve(*alias),
528                    column_aliases: column_aliases
529                        .as_ref()
530                        .map(|cols| cols.iter().map(|s| self.resolve(*s)).collect()),
531                }
532            }
533        }
534    }
535
536    fn convert_group_by(&self, gb: &arena_select::GroupByClause<'arena>) -> GroupByClause {
537        match gb {
538            arena_select::GroupByClause::Simple(exprs) => {
539                GroupByClause::Simple(exprs.iter().map(|e| self.convert_expression(e)).collect())
540            }
541            arena_select::GroupByClause::Rollup(elements) => GroupByClause::Rollup(
542                elements.iter().map(|e| self.convert_grouping_element(e)).collect(),
543            ),
544            arena_select::GroupByClause::Cube(elements) => GroupByClause::Cube(
545                elements.iter().map(|e| self.convert_grouping_element(e)).collect(),
546            ),
547            arena_select::GroupByClause::GroupingSets(sets) => GroupByClause::GroupingSets(
548                sets.iter().map(|s| self.convert_grouping_set(s)).collect(),
549            ),
550            arena_select::GroupByClause::Mixed(items) => GroupByClause::Mixed(
551                items.iter().map(|i| self.convert_mixed_grouping_item(i)).collect(),
552            ),
553        }
554    }
555
556    fn convert_grouping_element(
557        &self,
558        ge: &arena_select::GroupingElement<'arena>,
559    ) -> GroupingElement {
560        match ge {
561            arena_select::GroupingElement::Single(expr) => {
562                GroupingElement::Single(self.convert_expression(expr))
563            }
564            arena_select::GroupingElement::Composite(exprs) => GroupingElement::Composite(
565                exprs.iter().map(|e| self.convert_expression(e)).collect(),
566            ),
567        }
568    }
569
570    fn convert_grouping_set(&self, gs: &arena_select::GroupingSet<'arena>) -> GroupingSet {
571        GroupingSet { columns: gs.columns.iter().map(|e| self.convert_expression(e)).collect() }
572    }
573
574    fn convert_mixed_grouping_item(
575        &self,
576        mgi: &arena_select::MixedGroupingItem<'arena>,
577    ) -> MixedGroupingItem {
578        match mgi {
579            arena_select::MixedGroupingItem::Simple(expr) => {
580                MixedGroupingItem::Simple(self.convert_expression(expr))
581            }
582            arena_select::MixedGroupingItem::Rollup(elements) => MixedGroupingItem::Rollup(
583                elements.iter().map(|e| self.convert_grouping_element(e)).collect(),
584            ),
585            arena_select::MixedGroupingItem::Cube(elements) => MixedGroupingItem::Cube(
586                elements.iter().map(|e| self.convert_grouping_element(e)).collect(),
587            ),
588            arena_select::MixedGroupingItem::GroupingSets(sets) => MixedGroupingItem::GroupingSets(
589                sets.iter().map(|s| self.convert_grouping_set(s)).collect(),
590            ),
591        }
592    }
593
594    fn convert_set_operation(&self, so: &arena_select::SetOperation<'arena>) -> SetOperation {
595        SetOperation {
596            op: so.op.into(),
597            all: so.all,
598            right: Box::new(self.convert_select(so.right)),
599        }
600    }
601
602    // ========================================================================
603    // DML Statement Conversion
604    // ========================================================================
605
606    /// Convert an arena InsertStmt to an owned InsertStmt.
607    pub fn convert_insert(&self, stmt: &arena_dml::InsertStmt<'arena>) -> InsertStmt {
608        InsertStmt {
609            with_clause: stmt.with_clause.as_ref().map(|ctes| {
610                ctes.iter().map(|cte| self.convert_cte(cte)).collect()
611            }),
612            schema_name: stmt.schema_name.map(|s| self.resolve(s)),
613            schema_quoted: stmt.schema_quoted,
614            table_name: self.resolve(stmt.table_name),
615            table_quoted: stmt.table_quoted,
616            columns: stmt.columns.iter().map(|s| self.resolve(*s)).collect(),
617            source: self.convert_insert_source(&stmt.source),
618            conflict_clause: stmt.conflict_clause.map(ConflictClause::from),
619            on_conflict: stmt.on_conflict.as_ref().map(|c| self.convert_on_conflict_clause(c)),
620            on_duplicate_key_update: stmt.on_duplicate_key_update.as_ref().map(|assignments| {
621                assignments.iter().map(|a| self.convert_assignment(a)).collect()
622            }),
623        }
624    }
625
626    fn convert_on_conflict_clause(
627        &self,
628        clause: &arena_dml::OnConflictClause<'arena>,
629    ) -> crate::OnConflictClause {
630        crate::OnConflictClause {
631            conflict_target: clause
632                .conflict_target
633                .as_ref()
634                .map(|cols| cols.iter().map(|s| self.resolve(*s)).collect()),
635            action: match &clause.action {
636                arena_dml::OnConflictAction::DoNothing => crate::OnConflictAction::DoNothing,
637                arena_dml::OnConflictAction::DoUpdate { assignments, where_clause } => {
638                    crate::OnConflictAction::DoUpdate {
639                        assignments: assignments
640                            .iter()
641                            .map(|a| self.convert_assignment(a))
642                            .collect(),
643                        where_clause: where_clause.as_ref().map(|e| self.convert_expression(e)),
644                    }
645                }
646            },
647        }
648    }
649
650    fn convert_insert_source(&self, source: &arena_dml::InsertSource<'arena>) -> InsertSource {
651        match source {
652            arena_dml::InsertSource::Values(rows) => InsertSource::Values(
653                rows.iter()
654                    .map(|row| row.iter().map(|e| self.convert_expression(e)).collect())
655                    .collect(),
656            ),
657            arena_dml::InsertSource::Select(query) => {
658                InsertSource::Select(Box::new(self.convert_select(query)))
659            }
660            arena_dml::InsertSource::DefaultValues => InsertSource::DefaultValues,
661        }
662    }
663
664    fn convert_assignment(&self, a: &arena_dml::Assignment<'arena>) -> Assignment {
665        Assignment { column: self.resolve(a.column), value: self.convert_expression(&a.value) }
666    }
667
668    /// Convert an arena UpdateStmt to an owned UpdateStmt.
669    pub fn convert_update(&self, stmt: &arena_dml::UpdateStmt<'arena>) -> UpdateStmt {
670        UpdateStmt {
671            with_clause: stmt.with_clause.as_ref().map(|ctes| {
672                ctes.iter().map(|cte| self.convert_cte(cte)).collect()
673            }),
674            table_name: self.resolve(stmt.table_name),
675            quoted: stmt.quoted,
676            alias: stmt.alias.map(|a| self.resolve(a)),
677            assignments: stmt.assignments.iter().map(|a| self.convert_assignment(a)).collect(),
678            from_clause: stmt.from_clause.as_ref().map(|froms| {
679                froms.iter().map(|f| self.convert_from_clause(f)).collect()
680            }),
681            where_clause: stmt.where_clause.as_ref().map(|wc| self.convert_where_clause(wc)),
682            conflict_clause: stmt.conflict_clause.map(ConflictClause::from),
683        }
684    }
685
686    fn convert_where_clause(&self, wc: &arena_dml::WhereClause<'arena>) -> WhereClause {
687        match wc {
688            arena_dml::WhereClause::Condition(expr) => {
689                WhereClause::Condition(self.convert_expression(expr))
690            }
691            arena_dml::WhereClause::CurrentOf(cursor) => {
692                WhereClause::CurrentOf(self.resolve(*cursor))
693            }
694        }
695    }
696
697    /// Convert an arena DeleteStmt to an owned DeleteStmt.
698    pub fn convert_delete(&self, stmt: &arena_dml::DeleteStmt<'arena>) -> DeleteStmt {
699        DeleteStmt {
700            with_clause: stmt.with_clause.as_ref().map(|ctes| {
701                ctes.iter().map(|cte| self.convert_cte(cte)).collect()
702            }),
703            only: stmt.only,
704            table_name: self.resolve(stmt.table_name),
705            quoted: stmt.quoted,
706            where_clause: stmt.where_clause.as_ref().map(|wc| self.convert_where_clause(wc)),
707            order_by: stmt
708                .order_by
709                .as_ref()
710                .map(|v| v.iter().map(|item| self.convert_order_by_item(item)).collect()),
711            limit: stmt.limit.as_ref().map(|e| self.convert_expression(e)),
712            offset: stmt.offset.as_ref().map(|e| self.convert_expression(e)),
713        }
714    }
715}
716
717// ============================================================================
718// Simple From implementations for enums (don't need interner)
719// ============================================================================
720
721impl From<arena_expr::CharacterUnit> for CharacterUnit {
722    fn from(u: arena_expr::CharacterUnit) -> Self {
723        match u {
724            arena_expr::CharacterUnit::Characters => CharacterUnit::Characters,
725            arena_expr::CharacterUnit::Octets => CharacterUnit::Octets,
726        }
727    }
728}
729
730impl From<arena_expr::TrimPosition> for TrimPosition {
731    fn from(p: arena_expr::TrimPosition) -> Self {
732        match p {
733            arena_expr::TrimPosition::Both => TrimPosition::Both,
734            arena_expr::TrimPosition::Leading => TrimPosition::Leading,
735            arena_expr::TrimPosition::Trailing => TrimPosition::Trailing,
736        }
737    }
738}
739
740impl From<arena_expr::IntervalUnit> for IntervalUnit {
741    fn from(u: arena_expr::IntervalUnit) -> Self {
742        match u {
743            arena_expr::IntervalUnit::Microsecond => IntervalUnit::Microsecond,
744            arena_expr::IntervalUnit::Second => IntervalUnit::Second,
745            arena_expr::IntervalUnit::Minute => IntervalUnit::Minute,
746            arena_expr::IntervalUnit::Hour => IntervalUnit::Hour,
747            arena_expr::IntervalUnit::Day => IntervalUnit::Day,
748            arena_expr::IntervalUnit::Week => IntervalUnit::Week,
749            arena_expr::IntervalUnit::Month => IntervalUnit::Month,
750            arena_expr::IntervalUnit::Quarter => IntervalUnit::Quarter,
751            arena_expr::IntervalUnit::Year => IntervalUnit::Year,
752            arena_expr::IntervalUnit::SecondMicrosecond => IntervalUnit::SecondMicrosecond,
753            arena_expr::IntervalUnit::MinuteMicrosecond => IntervalUnit::MinuteMicrosecond,
754            arena_expr::IntervalUnit::MinuteSecond => IntervalUnit::MinuteSecond,
755            arena_expr::IntervalUnit::HourMicrosecond => IntervalUnit::HourMicrosecond,
756            arena_expr::IntervalUnit::HourSecond => IntervalUnit::HourSecond,
757            arena_expr::IntervalUnit::HourMinute => IntervalUnit::HourMinute,
758            arena_expr::IntervalUnit::DayMicrosecond => IntervalUnit::DayMicrosecond,
759            arena_expr::IntervalUnit::DaySecond => IntervalUnit::DaySecond,
760            arena_expr::IntervalUnit::DayMinute => IntervalUnit::DayMinute,
761            arena_expr::IntervalUnit::DayHour => IntervalUnit::DayHour,
762            arena_expr::IntervalUnit::YearMonth => IntervalUnit::YearMonth,
763        }
764    }
765}
766
767impl From<arena_expr::Quantifier> for Quantifier {
768    fn from(q: arena_expr::Quantifier) -> Self {
769        match q {
770            arena_expr::Quantifier::All => Quantifier::All,
771            arena_expr::Quantifier::Any => Quantifier::Any,
772            arena_expr::Quantifier::Some => Quantifier::Some,
773        }
774    }
775}
776
777impl From<arena_expr::TruthValue> for TruthValue {
778    fn from(tv: arena_expr::TruthValue) -> Self {
779        match tv {
780            arena_expr::TruthValue::True => TruthValue::True,
781            arena_expr::TruthValue::False => TruthValue::False,
782            arena_expr::TruthValue::Unknown => TruthValue::Unknown,
783        }
784    }
785}
786
787impl From<arena_expr::FulltextMode> for FulltextMode {
788    fn from(m: arena_expr::FulltextMode) -> Self {
789        match m {
790            arena_expr::FulltextMode::NaturalLanguage => FulltextMode::NaturalLanguage,
791            arena_expr::FulltextMode::Boolean => FulltextMode::Boolean,
792            arena_expr::FulltextMode::QueryExpansion => FulltextMode::QueryExpansion,
793        }
794    }
795}
796
797impl From<arena_expr::PseudoTable> for PseudoTable {
798    fn from(p: arena_expr::PseudoTable) -> Self {
799        match p {
800            arena_expr::PseudoTable::Old => PseudoTable::Old,
801            arena_expr::PseudoTable::New => PseudoTable::New,
802        }
803    }
804}
805
806impl From<arena_expr::FrameUnit> for FrameUnit {
807    fn from(u: arena_expr::FrameUnit) -> Self {
808        match u {
809            arena_expr::FrameUnit::Rows => FrameUnit::Rows,
810            arena_expr::FrameUnit::Range => FrameUnit::Range,
811        }
812    }
813}
814
815impl From<arena_expr::OrderDirection> for OrderDirection {
816    fn from(d: arena_expr::OrderDirection) -> Self {
817        match d {
818            arena_expr::OrderDirection::Asc => OrderDirection::Asc,
819            arena_expr::OrderDirection::Desc => OrderDirection::Desc,
820        }
821    }
822}
823
824impl From<arena_select::JoinType> for JoinType {
825    fn from(jt: arena_select::JoinType) -> Self {
826        match jt {
827            arena_select::JoinType::Inner => JoinType::Inner,
828            arena_select::JoinType::LeftOuter => JoinType::LeftOuter,
829            arena_select::JoinType::RightOuter => JoinType::RightOuter,
830            arena_select::JoinType::FullOuter => JoinType::FullOuter,
831            arena_select::JoinType::Cross => JoinType::Cross,
832            arena_select::JoinType::Semi => JoinType::Semi,
833            arena_select::JoinType::Anti => JoinType::Anti,
834        }
835    }
836}
837
838impl From<arena_select::SetOperator> for SetOperator {
839    fn from(op: arena_select::SetOperator) -> Self {
840        match op {
841            arena_select::SetOperator::Union => SetOperator::Union,
842            arena_select::SetOperator::Intersect => SetOperator::Intersect,
843            arena_select::SetOperator::Except => SetOperator::Except,
844        }
845    }
846}
847
848impl From<arena_dml::ConflictClause> for ConflictClause {
849    fn from(cc: arena_dml::ConflictClause) -> Self {
850        match cc {
851            arena_dml::ConflictClause::Abort => ConflictClause::Abort,
852            arena_dml::ConflictClause::Fail => ConflictClause::Fail,
853            arena_dml::ConflictClause::Ignore => ConflictClause::Ignore,
854            arena_dml::ConflictClause::Replace => ConflictClause::Replace,
855            arena_dml::ConflictClause::Rollback => ConflictClause::Rollback,
856        }
857    }
858}
859
860#[cfg(test)]
861mod tests {
862    use bumpalo::{collections::Vec as BumpVec, Bump};
863
864    use super::*;
865
866    /// Helper to create a Conjunction in the arena
867    fn make_conjunction<'arena>(
868        arena: &'arena Bump,
869        exprs: Vec<arena_expr::Expression<'arena>>,
870    ) -> arena_expr::Expression<'arena> {
871        let mut children = BumpVec::new_in(arena);
872        for e in exprs {
873            children.push(e);
874        }
875        arena_expr::Expression::Conjunction(children)
876    }
877
878    /// Helper to create a Disjunction in the arena
879    fn make_disjunction<'arena>(
880        arena: &'arena Bump,
881        exprs: Vec<arena_expr::Expression<'arena>>,
882    ) -> arena_expr::Expression<'arena> {
883        let mut children = BumpVec::new_in(arena);
884        for e in exprs {
885            children.push(e);
886        }
887        arena_expr::Expression::Disjunction(children)
888    }
889
890    /// Verify that Conjunction is normalized to nested BinaryOp::And
891    #[test]
892    fn test_conjunction_normalizes_to_nested_binary_and() {
893        let arena = Bump::new();
894        let interner = ArenaInterner::new(&arena);
895        let converter = Converter::new(&interner);
896
897        // Create a Conjunction with 3 children: a AND b AND c
898        let a = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(1));
899        let b = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(2));
900        let c = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(3));
901
902        let conjunction = make_conjunction(&arena, vec![a, b, c]);
903        let result = converter.convert_expression(&conjunction);
904
905        // Should produce: ((1 AND 2) AND 3)
906        match result {
907            Expression::BinaryOp { op: BinaryOperator::And, left, right } => {
908                // right should be 3
909                assert!(matches!(*right, Expression::Literal(vibesql_types::SqlValue::Integer(3))));
910                // left should be (1 AND 2)
911                match *left {
912                    Expression::BinaryOp { op: BinaryOperator::And, left: ll, right: lr } => {
913                        assert!(matches!(
914                            *ll,
915                            Expression::Literal(vibesql_types::SqlValue::Integer(1))
916                        ));
917                        assert!(matches!(
918                            *lr,
919                            Expression::Literal(vibesql_types::SqlValue::Integer(2))
920                        ));
921                    }
922                    _ => panic!("Expected nested BinaryOp::And, got {:?}", left),
923                }
924            }
925            _ => panic!("Expected BinaryOp::And, got {:?}", result),
926        }
927    }
928
929    /// Verify that Disjunction is normalized to nested BinaryOp::Or
930    #[test]
931    fn test_disjunction_normalizes_to_nested_binary_or() {
932        let arena = Bump::new();
933        let interner = ArenaInterner::new(&arena);
934        let converter = Converter::new(&interner);
935
936        // Create a Disjunction with 3 children: a OR b OR c
937        let a = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(1));
938        let b = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(2));
939        let c = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(3));
940
941        let disjunction = make_disjunction(&arena, vec![a, b, c]);
942        let result = converter.convert_expression(&disjunction);
943
944        // Should produce: ((1 OR 2) OR 3)
945        match result {
946            Expression::BinaryOp { op: BinaryOperator::Or, left, right } => {
947                // right should be 3
948                assert!(matches!(*right, Expression::Literal(vibesql_types::SqlValue::Integer(3))));
949                // left should be (1 OR 2)
950                match *left {
951                    Expression::BinaryOp { op: BinaryOperator::Or, left: ll, right: lr } => {
952                        assert!(matches!(
953                            *ll,
954                            Expression::Literal(vibesql_types::SqlValue::Integer(1))
955                        ));
956                        assert!(matches!(
957                            *lr,
958                            Expression::Literal(vibesql_types::SqlValue::Integer(2))
959                        ));
960                    }
961                    _ => panic!("Expected nested BinaryOp::Or, got {:?}", left),
962                }
963            }
964            _ => panic!("Expected BinaryOp::Or, got {:?}", result),
965        }
966    }
967
968    /// Verify that 2-element Conjunction produces simple BinaryOp::And
969    #[test]
970    fn test_two_element_conjunction() {
971        let arena = Bump::new();
972        let interner = ArenaInterner::new(&arena);
973        let converter = Converter::new(&interner);
974
975        let a = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(1));
976        let b = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(2));
977
978        let conjunction = make_conjunction(&arena, vec![a, b]);
979        let result = converter.convert_expression(&conjunction);
980
981        // Should produce: (1 AND 2)
982        match result {
983            Expression::BinaryOp { op: BinaryOperator::And, left, right } => {
984                assert!(matches!(*left, Expression::Literal(vibesql_types::SqlValue::Integer(1))));
985                assert!(matches!(*right, Expression::Literal(vibesql_types::SqlValue::Integer(2))));
986            }
987            _ => panic!("Expected BinaryOp::And, got {:?}", result),
988        }
989    }
990
991    /// Verify that 4-element Conjunction produces left-associative tree
992    #[test]
993    fn test_four_element_conjunction_is_left_associative() {
994        let arena = Bump::new();
995        let interner = ArenaInterner::new(&arena);
996        let converter = Converter::new(&interner);
997
998        // a AND b AND c AND d -> (((a AND b) AND c) AND d)
999        let a = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(1));
1000        let b = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(2));
1001        let c = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(3));
1002        let d = arena_expr::Expression::Literal(vibesql_types::SqlValue::Integer(4));
1003
1004        let conjunction = make_conjunction(&arena, vec![a, b, c, d]);
1005        let result = converter.convert_expression(&conjunction);
1006
1007        // Verify structure: (((1 AND 2) AND 3) AND 4)
1008        // Count depth on the left side - should be 3 levels for 4 elements
1009        fn count_left_depth(expr: &Expression) -> usize {
1010            match expr {
1011                Expression::BinaryOp { op: BinaryOperator::And, left, .. } => {
1012                    1 + count_left_depth(left)
1013                }
1014                _ => 0,
1015            }
1016        }
1017
1018        assert_eq!(count_left_depth(&result), 3, "Expected 3 levels of nesting for 4 elements");
1019    }
1020}