Skip to main content

cqlite_core/query/
select_ast.rs

1//! CQL SELECT Abstract Syntax Tree.
2//!
3//! AST types for SELECT statements executed directly against SSTable files.
4//! Covers projections, WHERE expressions, aggregates, GROUP BY/HAVING,
5//! ORDER BY, LIMIT/OFFSET, collection access, and arithmetic expressions.
6
7use crate::{Error, Result, TableId, Value};
8use serde::{Deserialize, Serialize};
9
10/// Complete SELECT statement AST
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct SelectStatement {
13    /// SELECT clause - what to return
14    pub select_clause: SelectClause,
15    /// FROM clause - which table(s) to query (optional for constant expressions)
16    pub from_clause: Option<FromClause>,
17    /// WHERE clause - filtering conditions
18    pub where_clause: Option<WhereExpression>,
19    /// GROUP BY clause - grouping columns
20    pub group_by: Option<GroupByClause>,
21    /// HAVING clause - filtering after grouping
22    pub having_clause: Option<WhereExpression>,
23    /// ORDER BY clause - sorting specification
24    pub order_by: Option<OrderByClause>,
25    /// LIMIT clause - query-wide result size limitation
26    pub limit: Option<LimitClause>,
27    /// PER PARTITION LIMIT - cap on rows returned per partition, applied
28    /// before the query-wide `limit` (Cassandra semantics, Issue #757)
29    pub per_partition_limit: Option<u64>,
30    /// OFFSET clause - result pagination
31    pub offset: Option<u64>,
32    /// Allow filtering flag (for non-indexed queries)
33    pub allow_filtering: bool,
34}
35
36/// SELECT clause - defines what columns/expressions to return
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub enum SelectClause {
39    /// SELECT * - all columns
40    All,
41    /// SELECT column1, column2, ... - specific columns
42    Columns(Vec<SelectExpression>),
43    /// SELECT DISTINCT column1, column2, ... - unique values only
44    Distinct(Vec<SelectExpression>),
45}
46
47/// Expression in SELECT clause
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub enum SelectExpression {
50    /// Simple column reference
51    Column(ColumnRef),
52    /// Aggregate function
53    Aggregate(AggregateFunction),
54    /// Scalar function
55    Function(FunctionCall),
56    /// `WRITETIME(col)` or `TTL(col)` — first-class metadata-retrieval functions.
57    ///
58    /// Using a dedicated variant avoids downstream string-matching on the function
59    /// name and keeps the executor dispatch explicit and exhaustive.
60    WriteTimeTtl(WriteTimeTtlCall),
61    /// Literal value
62    Literal(Value),
63    /// Positional bind marker (`?`) carrying its 0-based index in the statement.
64    ///
65    /// Issue #961: produced by the SELECT parser whenever it encounters a `?`
66    /// placeholder in a value position (e.g. the RHS of a WHERE comparison). It
67    /// is a *transient* node: parameter binding
68    /// (`bind_parameters`) rewrites every `BindMarker(i)` into the corresponding
69    /// `Literal(params[i])` before the statement reaches the optimizer or
70    /// executor. Reaching execution with an unbound marker is a logic error and
71    /// surfaces as a query-execution error rather than a panic.
72    BindMarker(usize),
73    /// Collection access (list[0], map['key'])
74    CollectionAccess(CollectionAccessExpression),
75    /// Arithmetic expression
76    Arithmetic(ArithmeticExpression),
77    /// Aliased expression (expr AS alias)
78    Aliased(Box<SelectExpression>, String),
79}
80
81/// Column reference with optional table qualifier
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub struct ColumnRef {
84    /// Table name (optional for simple queries)
85    pub table: Option<String>,
86    /// Column name
87    pub column: String,
88}
89
90/// Aggregate function call
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct AggregateFunction {
93    /// Function name (COUNT, SUM, AVG, MIN, MAX)
94    pub function: AggregateType,
95    /// Arguments (usually column references)
96    pub args: Vec<SelectExpression>,
97    /// DISTINCT modifier
98    pub distinct: bool,
99}
100
101/// Types of aggregate functions
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103pub enum AggregateType {
104    Count,
105    Sum,
106    Avg,
107    Min,
108    Max,
109}
110
111/// Scalar function call
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct FunctionCall {
114    /// Function name
115    pub name: String,
116    /// Arguments
117    pub args: Vec<SelectExpression>,
118}
119
120/// The two metadata-retrieval functions Cassandra exposes in SELECT.
121///
122/// These are first-class variants rather than being folded into `FunctionCall`
123/// so the executor can dispatch on them without string-matching function names.
124///
125/// # Executor TODO (#692)
126/// Evaluation is not yet wired: the executor must thread `writetime` / `ttl`
127/// cell-level metadata from `SSTableReader` up through the row-scanning loop
128/// and then return `Value::BigInt(micros)` / `Value::Int(seconds)` respectively.
129/// Until that work lands, selecting these columns returns `Value::Null`.
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub enum WriteTimeTtlFunction {
132    /// `WRITETIME(col)` — returns the write timestamp in microseconds (bigint)
133    WriteTime,
134    /// `TTL(col)` — returns the remaining TTL in seconds (int), or NULL if no TTL
135    Ttl,
136}
137
138/// A parsed `WRITETIME(col)` or `TTL(col)` select item.
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub struct WriteTimeTtlCall {
141    /// Which function was written
142    pub function: WriteTimeTtlFunction,
143    /// The single column argument (case-preserved from the source text)
144    pub column: String,
145    /// Optional alias (`WRITETIME(col) AS wt`)
146    pub alias: Option<String>,
147}
148
149/// Collection access operations
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151pub enum CollectionAccessExpression {
152    /// List element access: list[index]
153    ListIndex(ColumnRef, Box<SelectExpression>),
154    /// Map value access: map['key']
155    MapKey(ColumnRef, Box<SelectExpression>),
156    /// Set membership test: value IN set_column
157    SetContains(ColumnRef, Box<SelectExpression>),
158}
159
160/// Arithmetic expressions
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162pub struct ArithmeticExpression {
163    /// Left operand
164    pub left: Box<SelectExpression>,
165    /// Operator
166    pub operator: ArithmeticOperator,
167    /// Right operand
168    pub right: Box<SelectExpression>,
169}
170
171/// Arithmetic operators
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub enum ArithmeticOperator {
174    Add,
175    Subtract,
176    Multiply,
177    Divide,
178    Modulo,
179}
180
181/// FROM clause. Cassandra CQL only supports single-table queries (no JOINs).
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub enum FromClause {
184    /// Single table
185    Table(TableId),
186    /// Table with alias (Cassandra CQL supports table aliases)
187    TableAlias(TableId, String),
188}
189
190/// Advanced WHERE expression tree
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[allow(clippy::large_enum_variant)]
193pub enum WhereExpression {
194    /// Simple comparison
195    Comparison(ComparisonExpression),
196    /// Logical AND
197    And(Vec<WhereExpression>),
198    /// Logical OR  
199    Or(Vec<WhereExpression>),
200    /// Logical NOT
201    Not(Box<WhereExpression>),
202    /// Parenthesized expression
203    Parentheses(Box<WhereExpression>),
204}
205
206/// Comparison expression
207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
208pub struct ComparisonExpression {
209    /// Left side (usually column)
210    pub left: SelectExpression,
211    /// Comparison operator
212    pub operator: ComparisonOperator,
213    /// Right side (value, column, or expression)
214    pub right: ComparisonRightSide,
215}
216
217/// Right side of comparison
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219pub enum ComparisonRightSide {
220    /// Single value
221    Value(SelectExpression),
222    /// List of values for IN/NOT IN
223    ValueList(Vec<SelectExpression>),
224    /// Range for BETWEEN
225    Range(SelectExpression, SelectExpression),
226}
227
228/// Comparison operators
229#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
230pub enum ComparisonOperator {
231    /// Equality
232    Equal,
233    /// Inequality
234    NotEqual,
235    /// Less than
236    LessThan,
237    /// Less than or equal
238    LessThanOrEqual,
239    /// Greater than
240    GreaterThan,
241    /// Greater than or equal
242    GreaterThanOrEqual,
243    /// IN operator
244    In,
245    /// NOT IN operator
246    NotIn,
247    /// LIKE operator (pattern matching)
248    Like,
249    /// NOT LIKE operator
250    NotLike,
251    /// BETWEEN operator
252    Between,
253    /// NOT BETWEEN operator
254    NotBetween,
255    /// IS NULL
256    IsNull,
257    /// IS NOT NULL
258    IsNotNull,
259    /// Regular expression matching
260    Regex,
261    /// Collection CONTAINS
262    Contains,
263    /// Collection CONTAINS KEY
264    ContainsKey,
265}
266
267/// GROUP BY clause
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269pub struct GroupByClause {
270    /// Columns to group by
271    pub columns: Vec<ColumnRef>,
272}
273
274/// ORDER BY clause
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
276pub struct OrderByClause {
277    /// Order specifications
278    pub items: Vec<OrderByItem>,
279}
280
281/// Individual ORDER BY item
282#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
283pub struct OrderByItem {
284    /// Expression to order by
285    pub expression: SelectExpression,
286    /// Sort direction
287    pub direction: SortDirection,
288}
289
290/// Sort direction
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292pub enum SortDirection {
293    Ascending,
294    Descending,
295}
296
297/// LIMIT clause
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299pub struct LimitClause {
300    /// Maximum number of rows
301    pub count: u64,
302}
303
304impl SelectStatement {
305    /// Create a simple SELECT * FROM table statement
306    pub fn select_all_from(table: TableId) -> Self {
307        Self {
308            select_clause: SelectClause::All,
309            from_clause: Some(FromClause::Table(table)),
310            where_clause: None,
311            group_by: None,
312            having_clause: None,
313            order_by: None,
314            limit: None,
315            per_partition_limit: None,
316            offset: None,
317            allow_filtering: false,
318        }
319    }
320
321    /// Check if this query requires aggregation
322    pub fn requires_aggregation(&self) -> bool {
323        self.group_by.is_some() || self.has_aggregate_functions()
324    }
325
326    /// Check if this query has aggregate functions
327    pub fn has_aggregate_functions(&self) -> bool {
328        match &self.select_clause {
329            SelectClause::Columns(exprs) | SelectClause::Distinct(exprs) => {
330                exprs.iter().any(|expr| expr.is_aggregate())
331            }
332            SelectClause::All => false,
333        }
334    }
335
336    /// Count the positional `?` bind markers in this statement.
337    ///
338    /// Issue #961: the marker indices are assigned left-to-right by the parser,
339    /// so the highest index plus one equals the required parameter count. Used by
340    /// `execute_with_params` / prepared execution to enforce a strict arity check
341    /// before binding.
342    pub fn bind_marker_count(&self) -> usize {
343        let mut max_plus_one = 0usize;
344        if let SelectClause::Columns(exprs) | SelectClause::Distinct(exprs) = &self.select_clause {
345            for expr in exprs {
346                expr.scan_bind_markers(&mut max_plus_one);
347            }
348        }
349        if let Some(where_expr) = &self.where_clause {
350            where_expr.scan_bind_markers(&mut max_plus_one);
351        }
352        if let Some(having) = &self.having_clause {
353            having.scan_bind_markers(&mut max_plus_one);
354        }
355        max_plus_one
356    }
357
358    /// Substitute positional `?` bind markers with `params`, in place.
359    ///
360    /// Issue #961: each `SelectExpression::BindMarker(i)` is rewritten to
361    /// `SelectExpression::Literal(params[i].clone())`. The supplied parameter
362    /// count must exactly equal `bind_marker_count()`; too few or too many is a
363    /// hard error (strict CQL arity). Binding happens *before* optimization, so
364    /// the bound literals participate in partition-key classification, encoding,
365    /// and typed coercion exactly as if they had been written inline.
366    pub fn bind_parameters(&mut self, params: &[Value]) -> Result<()> {
367        let expected = self.bind_marker_count();
368        if params.len() != expected {
369            return Err(Error::query_execution(format!(
370                "Parameter count mismatch: query has {expected} bind marker(s), got {} parameter(s)",
371                params.len()
372            )));
373        }
374        if let SelectClause::Columns(exprs) | SelectClause::Distinct(exprs) =
375            &mut self.select_clause
376        {
377            for expr in exprs.iter_mut() {
378                expr.bind_parameters(params)?;
379            }
380        }
381        if let Some(where_expr) = &mut self.where_clause {
382            where_expr.bind_parameters(params)?;
383        }
384        if let Some(having) = &mut self.having_clause {
385            having.bind_parameters(params)?;
386        }
387        Ok(())
388    }
389
390    /// Get all referenced columns (for query planning).
391    ///
392    /// `SELECT *` contributes nothing here; the projection is resolved later
393    /// against the schema during planning.
394    pub fn get_referenced_columns(&self) -> Vec<ColumnRef> {
395        let mut columns = Vec::new();
396
397        if let SelectClause::Columns(exprs) | SelectClause::Distinct(exprs) = &self.select_clause {
398            for expr in exprs {
399                columns.extend(expr.get_column_refs());
400            }
401        }
402
403        if let Some(where_expr) = &self.where_clause {
404            columns.extend(where_expr.get_column_refs());
405        }
406
407        if let Some(group_by) = &self.group_by {
408            columns.extend(group_by.columns.iter().cloned());
409        }
410
411        if let Some(having) = &self.having_clause {
412            columns.extend(having.get_column_refs());
413        }
414
415        if let Some(order_by) = &self.order_by {
416            for item in &order_by.items {
417                columns.extend(item.expression.get_column_refs());
418            }
419        }
420
421        columns
422    }
423}
424
425impl SelectExpression {
426    /// Check if this expression is an aggregate function.
427    ///
428    /// An aliased aggregate (`COUNT(*) AS total`) is still an aggregate: unwrap
429    /// `Aliased` so `SELECT COUNT(*) AS total` is planned through the aggregation
430    /// step rather than falling into row-level projection (issue #1763).
431    pub fn is_aggregate(&self) -> bool {
432        match self {
433            SelectExpression::Aggregate(_) => true,
434            SelectExpression::Aliased(inner, _) => inner.is_aggregate(),
435            _ => false,
436        }
437    }
438
439    /// Update `max_plus_one` to `max(current, marker_index + 1)` over every
440    /// `BindMarker` reachable from this expression (Issue #961).
441    fn scan_bind_markers(&self, max_plus_one: &mut usize) {
442        match self {
443            SelectExpression::BindMarker(idx) => *max_plus_one = (*max_plus_one).max(idx + 1),
444            SelectExpression::Aggregate(agg) => {
445                for arg in &agg.args {
446                    arg.scan_bind_markers(max_plus_one);
447                }
448            }
449            SelectExpression::Function(func) => {
450                for arg in &func.args {
451                    arg.scan_bind_markers(max_plus_one);
452                }
453            }
454            SelectExpression::CollectionAccess(access) => {
455                let (_, sub) = match access {
456                    CollectionAccessExpression::ListIndex(c, e)
457                    | CollectionAccessExpression::MapKey(c, e)
458                    | CollectionAccessExpression::SetContains(c, e) => (c, e),
459                };
460                sub.scan_bind_markers(max_plus_one);
461            }
462            SelectExpression::Arithmetic(arith) => {
463                arith.left.scan_bind_markers(max_plus_one);
464                arith.right.scan_bind_markers(max_plus_one);
465            }
466            SelectExpression::Aliased(expr, _) => expr.scan_bind_markers(max_plus_one),
467            SelectExpression::Column(_)
468            | SelectExpression::Literal(_)
469            | SelectExpression::WriteTimeTtl(_) => {}
470        }
471    }
472
473    /// Replace each `BindMarker(i)` reachable from this expression with
474    /// `Literal(params[i])` (Issue #961). Caller guarantees `params` covers every
475    /// marker index (`SelectStatement::bind_parameters` validates the count).
476    fn bind_parameters(&mut self, params: &[Value]) -> Result<()> {
477        match self {
478            SelectExpression::BindMarker(idx) => {
479                let value = params.get(*idx).ok_or_else(|| {
480                    Error::query_execution(format!(
481                        "Bind marker index {idx} has no corresponding parameter"
482                    ))
483                })?;
484                *self = SelectExpression::Literal(value.clone());
485            }
486            SelectExpression::Aggregate(agg) => {
487                for arg in agg.args.iter_mut() {
488                    arg.bind_parameters(params)?;
489                }
490            }
491            SelectExpression::Function(func) => {
492                for arg in func.args.iter_mut() {
493                    arg.bind_parameters(params)?;
494                }
495            }
496            SelectExpression::CollectionAccess(access) => {
497                let sub = match access {
498                    CollectionAccessExpression::ListIndex(_, e)
499                    | CollectionAccessExpression::MapKey(_, e)
500                    | CollectionAccessExpression::SetContains(_, e) => e,
501                };
502                sub.bind_parameters(params)?;
503            }
504            SelectExpression::Arithmetic(arith) => {
505                arith.left.bind_parameters(params)?;
506                arith.right.bind_parameters(params)?;
507            }
508            SelectExpression::Aliased(expr, _) => expr.bind_parameters(params)?,
509            SelectExpression::Column(_)
510            | SelectExpression::Literal(_)
511            | SelectExpression::WriteTimeTtl(_) => {}
512        }
513        Ok(())
514    }
515
516    /// Get all column references in this expression
517    pub fn get_column_refs(&self) -> Vec<ColumnRef> {
518        match self {
519            SelectExpression::Column(col_ref) => vec![col_ref.clone()],
520            SelectExpression::Aggregate(agg) => collect_refs(&agg.args),
521            SelectExpression::Function(func) => collect_refs(&func.args),
522            SelectExpression::WriteTimeTtl(call) => {
523                vec![ColumnRef::new(call.column.clone())]
524            }
525            SelectExpression::CollectionAccess(access) => {
526                let (col_ref, sub_expr) = match access {
527                    CollectionAccessExpression::ListIndex(c, e)
528                    | CollectionAccessExpression::MapKey(c, e)
529                    | CollectionAccessExpression::SetContains(c, e) => (c, e),
530                };
531                let mut refs = vec![col_ref.clone()];
532                refs.extend(sub_expr.get_column_refs());
533                refs
534            }
535            SelectExpression::Arithmetic(arith) => {
536                let mut refs = arith.left.get_column_refs();
537                refs.extend(arith.right.get_column_refs());
538                refs
539            }
540            SelectExpression::Aliased(expr, _) => expr.get_column_refs(),
541            SelectExpression::Literal(_) | SelectExpression::BindMarker(_) => Vec::new(),
542        }
543    }
544}
545
546/// Collect column refs from each expression in `exprs`, in order.
547fn collect_refs(exprs: &[SelectExpression]) -> Vec<ColumnRef> {
548    exprs
549        .iter()
550        .flat_map(SelectExpression::get_column_refs)
551        .collect()
552}
553
554impl WhereExpression {
555    /// Get all column references in this WHERE expression
556    pub fn get_column_refs(&self) -> Vec<ColumnRef> {
557        match self {
558            WhereExpression::Comparison(comp) => {
559                let mut refs = comp.left.get_column_refs();
560                match &comp.right {
561                    ComparisonRightSide::Value(expr) => {
562                        refs.extend(expr.get_column_refs());
563                    }
564                    ComparisonRightSide::ValueList(exprs) => {
565                        refs.extend(collect_refs(exprs));
566                    }
567                    ComparisonRightSide::Range(start, end) => {
568                        refs.extend(start.get_column_refs());
569                        refs.extend(end.get_column_refs());
570                    }
571                }
572                refs
573            }
574            WhereExpression::And(exprs) | WhereExpression::Or(exprs) => exprs
575                .iter()
576                .flat_map(WhereExpression::get_column_refs)
577                .collect(),
578            WhereExpression::Not(expr) | WhereExpression::Parentheses(expr) => {
579                expr.get_column_refs()
580            }
581        }
582    }
583
584    /// Update `max_plus_one` to cover every `BindMarker` in this WHERE tree
585    /// (Issue #961). Markers may appear on either side of a comparison.
586    fn scan_bind_markers(&self, max_plus_one: &mut usize) {
587        match self {
588            WhereExpression::Comparison(comp) => {
589                comp.left.scan_bind_markers(max_plus_one);
590                match &comp.right {
591                    ComparisonRightSide::Value(expr) => expr.scan_bind_markers(max_plus_one),
592                    ComparisonRightSide::ValueList(exprs) => {
593                        for expr in exprs {
594                            expr.scan_bind_markers(max_plus_one);
595                        }
596                    }
597                    ComparisonRightSide::Range(start, end) => {
598                        start.scan_bind_markers(max_plus_one);
599                        end.scan_bind_markers(max_plus_one);
600                    }
601                }
602            }
603            WhereExpression::And(exprs) | WhereExpression::Or(exprs) => {
604                for expr in exprs {
605                    expr.scan_bind_markers(max_plus_one);
606                }
607            }
608            WhereExpression::Not(expr) | WhereExpression::Parentheses(expr) => {
609                expr.scan_bind_markers(max_plus_one)
610            }
611        }
612    }
613
614    /// Replace each `BindMarker(i)` in this WHERE tree with `Literal(params[i])`
615    /// (Issue #961). Markers may appear on either side of a comparison and inside
616    /// `IN` value lists / `BETWEEN` ranges.
617    fn bind_parameters(&mut self, params: &[Value]) -> Result<()> {
618        match self {
619            WhereExpression::Comparison(comp) => {
620                comp.left.bind_parameters(params)?;
621                match &mut comp.right {
622                    ComparisonRightSide::Value(expr) => expr.bind_parameters(params)?,
623                    ComparisonRightSide::ValueList(exprs) => {
624                        for expr in exprs.iter_mut() {
625                            expr.bind_parameters(params)?;
626                        }
627                    }
628                    ComparisonRightSide::Range(start, end) => {
629                        start.bind_parameters(params)?;
630                        end.bind_parameters(params)?;
631                    }
632                }
633            }
634            WhereExpression::And(exprs) | WhereExpression::Or(exprs) => {
635                for expr in exprs.iter_mut() {
636                    expr.bind_parameters(params)?;
637                }
638            }
639            WhereExpression::Not(expr) | WhereExpression::Parentheses(expr) => {
640                expr.bind_parameters(params)?
641            }
642        }
643        Ok(())
644    }
645
646    /// Check if this WHERE expression can be pushed down to SSTable level.
647    ///
648    /// OR and NOT are excluded: efficient pushdown of those would require
649    /// index intersection / negative scans we don't currently support.
650    pub fn can_pushdown_to_sstable(&self) -> bool {
651        match self {
652            WhereExpression::Comparison(comp) => {
653                matches!(comp.left, SelectExpression::Column(_))
654                    && matches!(
655                        comp.operator,
656                        ComparisonOperator::Equal
657                            | ComparisonOperator::LessThan
658                            | ComparisonOperator::LessThanOrEqual
659                            | ComparisonOperator::GreaterThan
660                            | ComparisonOperator::GreaterThanOrEqual
661                            | ComparisonOperator::In
662                            | ComparisonOperator::Between
663                    )
664            }
665            WhereExpression::And(exprs) => {
666                exprs.iter().all(WhereExpression::can_pushdown_to_sstable)
667            }
668            WhereExpression::Or(_) | WhereExpression::Not(_) => false,
669            WhereExpression::Parentheses(expr) => expr.can_pushdown_to_sstable(),
670        }
671    }
672}
673
674impl ColumnRef {
675    /// Create a simple column reference
676    pub fn new(column: impl Into<String>) -> Self {
677        Self {
678            table: None,
679            column: column.into(),
680        }
681    }
682
683    /// Create a qualified column reference
684    pub fn qualified(table: impl Into<String>, column: impl Into<String>) -> Self {
685        Self {
686            table: Some(table.into()),
687            column: column.into(),
688        }
689    }
690}
691
692#[cfg(test)]
693mod tests {
694    use super::*;
695
696    #[test]
697    fn test_simple_select_statement() {
698        let stmt = SelectStatement::select_all_from(TableId::new("users"));
699        assert_eq!(stmt.select_clause, SelectClause::All);
700        assert!(!stmt.requires_aggregation());
701    }
702
703    #[test]
704    fn test_aggregate_detection() {
705        let stmt = SelectStatement {
706            select_clause: SelectClause::Columns(vec![SelectExpression::Aggregate(
707                AggregateFunction {
708                    function: AggregateType::Count,
709                    args: vec![SelectExpression::Column(ColumnRef::new("id"))],
710                    distinct: false,
711                },
712            )]),
713            from_clause: Some(FromClause::Table(TableId::new("users"))),
714            where_clause: None,
715            group_by: None,
716            having_clause: None,
717            order_by: None,
718            limit: None,
719            per_partition_limit: None,
720            offset: None,
721            allow_filtering: false,
722        };
723
724        assert!(stmt.requires_aggregation());
725        assert!(stmt.has_aggregate_functions());
726    }
727
728    #[test]
729    fn test_column_references() {
730        let where_expr = WhereExpression::And(vec![
731            WhereExpression::Comparison(ComparisonExpression {
732                left: SelectExpression::Column(ColumnRef::new("age")),
733                operator: ComparisonOperator::GreaterThan,
734                right: ComparisonRightSide::Value(SelectExpression::Literal(Value::Integer(21))),
735            }),
736            WhereExpression::Comparison(ComparisonExpression {
737                left: SelectExpression::Column(ColumnRef::new("city")),
738                operator: ComparisonOperator::Equal,
739                right: ComparisonRightSide::Value(SelectExpression::Literal(Value::Text(
740                    "NYC".to_string(),
741                ))),
742            }),
743        ]);
744
745        let column_refs = where_expr.get_column_refs();
746        assert_eq!(column_refs.len(), 2);
747        assert!(column_refs.iter().any(|col| col.column == "age"));
748        assert!(column_refs.iter().any(|col| col.column == "city"));
749    }
750
751    #[test]
752    fn test_pushdown_capability() {
753        let simple_comparison = WhereExpression::Comparison(ComparisonExpression {
754            left: SelectExpression::Column(ColumnRef::new("id")),
755            operator: ComparisonOperator::Equal,
756            right: ComparisonRightSide::Value(SelectExpression::Literal(Value::Integer(123))),
757        });
758
759        assert!(simple_comparison.can_pushdown_to_sstable());
760
761        let complex_or =
762            WhereExpression::Or(vec![simple_comparison.clone(), simple_comparison.clone()]);
763
764        assert!(!complex_or.can_pushdown_to_sstable());
765    }
766}