Skip to main content

uqa_sql/ast/
expressions.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7use serde::{Deserialize, Serialize};
8use uqa_core::Value;
9
10use super::{FunctionBinding, SelectStmt};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Projection {
14    pub expr: Expr,
15    pub alias: Option<String>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct OrderBy {
20    pub expr: Expr,
21    pub descending: bool,
22    /// `NULLS FIRST` / `NULLS LAST` placement. `None` means the
23    /// SQL-standard default - `NULLS LAST` for ASC and `NULLS FIRST`
24    /// for DESC. Mirrors `PostgreSQL` semantics.
25    pub nulls: Option<NullsOrder>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub enum NullsOrder {
30    First,
31    Last,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct WindowSpec {
36    pub partition_by: Vec<Expr>,
37    pub order_by: Vec<OrderBy>,
38    /// `ROWS` / `RANGE` frame, or `None` when not specified (defaults
39    /// to `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`).
40    pub frame: Option<WindowFrame>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct WindowFrame {
45    pub mode: FrameMode,
46    pub start: FrameBound,
47    pub end: FrameBound,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum FrameMode {
52    Rows,
53    Range,
54    Groups,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub enum FrameBound {
59    UnboundedPreceding,
60    UnboundedFollowing,
61    CurrentRow,
62    Preceding(Box<Expr>),
63    Following(Box<Expr>),
64}
65
66/// Scalar expression nodes the compiler handles.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub enum Expr {
69    Star,
70    /// Relation-qualified wildcard projection (`table.*` or `alias.*`).
71    QualifiedStar(String),
72    /// `DEFAULT` in an INSERT/UPDATE assignment. This is a mutation marker,
73    /// not a scalar value, and must be resolved against the target column
74    /// before expression evaluation.
75    Default,
76    /// Unqualified column reference (`col`).
77    Column(String),
78    /// Qualified column reference (`table.col` or `alias.col`).
79    QualifiedColumn {
80        qualifier: String,
81        column: String,
82    },
83    Literal(Value),
84    /// A positional bind parameter (`$1`, `$2`, ...).
85    Param(usize),
86    /// `text_match(...)`, `knn_match(...)`, etc. - dispatched through
87    /// the function registry.
88    Func {
89        name: String,
90        #[serde(default, skip_serializing_if = "Option::is_none")]
91        binding: Option<FunctionBinding>,
92        args: Vec<Expr>,
93        /// `func(DISTINCT expr)` - only meaningful for aggregate
94        /// functions. Mirrors `PostgreSQL`'s `agg_distinct`.
95        distinct: bool,
96        /// `func(expr ORDER BY ...)` - only meaningful for ordered
97        /// aggregates (`STRING_AGG`, `ARRAY_AGG`, `PERCENTILE_*`).
98        order_by: Vec<OrderBy>,
99        /// `func(...) FILTER (WHERE expr)` - aggregate-level row filter.
100        filter: Option<Box<Expr>>,
101    },
102    /// `ARRAY[1.0, 2.0, ...]` literal - currently restricted to numeric
103    /// elements (vectors).
104    Array(Vec<Expr>),
105    /// Anonymous SQL row constructor (`ROW(...)` or `(a, b)`).
106    Row(Vec<Expr>),
107    /// `lhs op rhs` - comparison or arithmetic.
108    Binary {
109        op: BinaryOp,
110        lhs: Box<Expr>,
111        rhs: Box<Expr>,
112    },
113    /// `PostgreSQL` prefix `-`, kept distinct from binary subtraction so the
114    /// operand's declared numeric width and overflow behavior survive lowering.
115    UnaryMinus(Box<Expr>),
116    /// `NOT expr`.
117    Not(Box<Expr>),
118    /// `cond_1 AND cond_2 AND ...` (n-ary).
119    And(Vec<Expr>),
120    /// `cond_1 OR cond_2 OR ...` (n-ary).
121    Or(Vec<Expr>),
122    /// `expr IS NULL` / `expr IS NOT NULL`.
123    IsNull {
124        expr: Box<Expr>,
125        negated: bool,
126    },
127    /// `expr BETWEEN low AND high`.
128    Between {
129        expr: Box<Expr>,
130        low: Box<Expr>,
131        high: Box<Expr>,
132    },
133    /// `expr IN (a, b, c)` literal list.
134    InList {
135        expr: Box<Expr>,
136        list: Vec<Expr>,
137        negated: bool,
138    },
139    /// `func(args) OVER (PARTITION BY ... ORDER BY ...)`.
140    WindowCall {
141        name: String,
142        args: Vec<Expr>,
143        spec: WindowSpec,
144    },
145    /// `CASE [base] WHEN cond THEN result ... [ELSE default] END`.
146    /// `base` lifts simple-form `CASE expr WHEN val THEN ...` into an
147    /// optional comparison anchor; searched-form `CASE WHEN cond ...`
148    /// leaves it `None`.
149    Case {
150        base: Option<Box<Expr>>,
151        when: Vec<(Expr, Expr)>,
152        else_branch: Option<Box<Expr>>,
153    },
154    /// `CAST(expr AS type)`. The type name is preserved verbatim so
155    /// the evaluator can apply the correct coercion.
156    Cast {
157        expr: Box<Expr>,
158        ty: String,
159    },
160    /// `(SELECT ...)` scalar subquery: yields a single row / single
161    /// column value at evaluation time.
162    ScalarSubquery(Box<SelectStmt>),
163    /// `EXISTS (SELECT ...)` -- truthy when the body produces at
164    /// least one row.
165    Exists {
166        body: Box<SelectStmt>,
167        negated: bool,
168    },
169    /// `expr [NOT] IN (SELECT ...)` set membership against a
170    /// subquery. Evaluator runs the body once per top-level
171    /// expression and tests membership.
172    InSubquery {
173        expr: Box<Expr>,
174        body: Box<SelectStmt>,
175        negated: bool,
176    },
177}
178
179impl Expr {
180    pub fn qualified_column(qualifier: impl Into<String>, column: impl Into<String>) -> Self {
181        Self::QualifiedColumn {
182            qualifier: qualifier.into(),
183            column: column.into(),
184        }
185    }
186
187    /// True when this expression tree contains a window function call.
188    #[must_use]
189    pub fn contains_window(&self) -> bool {
190        self.any_node(&|node| matches!(node, Self::WindowCall { .. }))
191    }
192
193    /// True when this expression tree contains a built-in aggregate call.
194    #[must_use]
195    pub fn contains_aggregate(&self) -> bool {
196        self.any_node(
197            &|node| matches!(node, Self::Func { name, .. } if is_builtin_aggregate_function(name)),
198        )
199    }
200
201    /// True when this expression contains a column whose owning relation can only be determined after catalog schemas have been bound.
202    #[must_use]
203    pub fn contains_unqualified_column(&self) -> bool {
204        self.any_node(&|node| matches!(node, Self::Column(_)))
205    }
206
207    /// True when this expression contains a function whose strictness cannot be decided without an engine catalog.
208    #[must_use]
209    pub fn contains_function_with_unknown_strictness(&self) -> bool {
210        self.any_node(&|node| {
211            matches!(
212                node,
213                Self::Func { name, args, .. }
214                    if crate::expr::builtin_scalar_function_strictness(name, args.len()).is_none()
215            )
216        })
217    }
218
219    /// Whether `hit` matches this node or any node below it. Subquery bodies are opaque: `ScalarSubquery` and `Exists` own their expression trees.
220    fn any_node(&self, hit: &dyn Fn(&Self) -> bool) -> bool {
221        if hit(self) {
222            return true;
223        }
224        match self {
225            Self::Func {
226                args,
227                order_by,
228                filter,
229                ..
230            } => {
231                args.iter().any(|arg| arg.any_node(hit))
232                    || order_by.iter().any(|order| order.expr.any_node(hit))
233                    || filter.as_deref().is_some_and(|filter| filter.any_node(hit))
234            }
235            Self::Array(items) | Self::Row(items) | Self::And(items) | Self::Or(items) => {
236                items.iter().any(|item| item.any_node(hit))
237            }
238            Self::UnaryMinus(expr) | Self::Not(expr) | Self::Cast { expr, .. } => {
239                expr.any_node(hit)
240            }
241            Self::Binary { lhs, rhs, .. } => lhs.any_node(hit) || rhs.any_node(hit),
242            Self::IsNull { expr, .. } | Self::InSubquery { expr, .. } => expr.any_node(hit),
243            Self::Between { expr, low, high } => {
244                expr.any_node(hit) || low.any_node(hit) || high.any_node(hit)
245            }
246            Self::InList { expr, list, .. } => {
247                expr.any_node(hit) || list.iter().any(|item| item.any_node(hit))
248            }
249            Self::Case {
250                base,
251                when,
252                else_branch,
253            } => {
254                base.as_deref().is_some_and(|base| base.any_node(hit))
255                    || when
256                        .iter()
257                        .any(|(condition, result)| condition.any_node(hit) || result.any_node(hit))
258                    || else_branch
259                        .as_deref()
260                        .is_some_and(|branch| branch.any_node(hit))
261            }
262            Self::WindowCall { .. }
263            | Self::Star
264            | Self::QualifiedStar(_)
265            | Self::Default
266            | Self::Column(_)
267            | Self::QualifiedColumn { .. }
268            | Self::Literal(_)
269            | Self::Param(_)
270            | Self::ScalarSubquery(_)
271            | Self::Exists { .. } => false,
272        }
273    }
274}
275
276/// Return whether `name` is a built-in aggregate understood by the planner.
277#[must_use]
278pub fn is_builtin_aggregate_function(name: &str) -> bool {
279    matches!(
280        name.to_ascii_lowercase().as_str(),
281        "count"
282            | "sum"
283            | "avg"
284            | "min"
285            | "max"
286            | "string_agg"
287            | "array_agg"
288            | "bool_and"
289            | "bool_or"
290            | "stddev"
291            | "stddev_samp"
292            | "stddev_pop"
293            | "variance"
294            | "var_samp"
295            | "var_pop"
296            | "percentile_cont"
297            | "percentile_disc"
298            | "mode"
299            | "json_agg"
300            | "jsonb_agg"
301            | "json_object_agg"
302            | "jsonb_object_agg"
303    )
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
307pub enum BinaryOp {
308    Equal,
309    NotEqual,
310    Less,
311    LessEqual,
312    Greater,
313    GreaterEqual,
314    Add,
315    Subtract,
316    Multiply,
317    Divide,
318}
319
320/// `Expr` restricted to value-producing forms used by `INSERT` rows.
321pub type ValueExpr = Expr;