Skip to main content

uqa_sql/ir/
mod.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Serializable scalar SQL IR shared by analysis, planning, and execution.
8
9mod call_arguments;
10mod traversal;
11pub use call_arguments::{
12    analyze_expression_call_arguments, scalar_call_argument, scalar_call_arguments,
13    scalar_call_arguments_with_control, validate_scalar_call_arguments, ScalarCallArgument,
14};
15
16use crate::ast::{
17    BinaryOp, ColumnType, FrameExclusion, FrameMode, FunctionBinding, FunctionOrderSyntax,
18    InternalColumnRef, NullsOrder, WindowCallModifiers,
19};
20use uqa_core::Value;
21
22/// Index into the query children owned by the enclosing expression plan.
23pub type SubqueryId = usize;
24
25#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
26pub enum ScalarExpr {
27    Star,
28    QualifiedStar(String),
29    Default,
30    Column(String),
31    /// Logical position in an already-bound physical row schema. This variant is introduced only after relational binding so duplicate SQL labels remain independently addressable.
32    Position(usize),
33    /// Structural executor-only attribute, resolved independently of SQL relation and column names.
34    InternalColumn(InternalColumnRef),
35    QualifiedColumn {
36        qualifier: String,
37        column: String,
38    },
39    Literal(Value),
40    /// An already-coerced runtime datum whose declared type must survive lowering.
41    TypedLiteral {
42        value: Value,
43        ty: String,
44        /// Resolved identity of an already-bound datum, including domain OIDs and type modifiers.
45        #[serde(default, skip_serializing_if = "Option::is_none")]
46        bound_type: Option<ColumnType>,
47        /// Original SQL parameter slot when specialization replaces a bare parameter.
48        #[serde(default, skip_serializing_if = "Option::is_none")]
49        parameter_index: Option<usize>,
50    },
51    Param(usize),
52    Func {
53        name: String,
54        #[serde(default, skip_serializing_if = "Option::is_none")]
55        binding: Option<FunctionBinding>,
56        args: Vec<Self>,
57        distinct: bool,
58        order_by: Vec<ScalarOrder>,
59        #[serde(default, skip_serializing_if = "FunctionOrderSyntax::is_legacy")]
60        order_syntax: FunctionOrderSyntax,
61        filter: Option<Box<Self>>,
62    },
63    Array(Vec<Self>),
64    Row(Vec<Self>),
65    CompositeRow {
66        items: Vec<Self>,
67        binding: crate::ast::CompositeRowBinding,
68        /// Resolved result identity for physical inference without catalog callbacks.
69        #[serde(default, skip_serializing_if = "Option::is_none")]
70        bound_type: Option<ColumnType>,
71    },
72    Binary {
73        op: BinaryOp,
74        lhs: Box<Self>,
75        rhs: Box<Self>,
76    },
77    UnaryMinus(Box<Self>),
78    Not(Box<Self>),
79    And(Vec<Self>),
80    Or(Vec<Self>),
81    IsNull {
82        expr: Box<Self>,
83        negated: bool,
84    },
85    Between {
86        expr: Box<Self>,
87        low: Box<Self>,
88        high: Box<Self>,
89    },
90    InList {
91        expr: Box<Self>,
92        list: Vec<Self>,
93        negated: bool,
94    },
95    /// A window function call; `filter` is an aggregate's `FILTER (WHERE ...)` condition.
96    WindowCall {
97        name: String,
98        args: Vec<Self>,
99        spec: ScalarWindowSpec,
100        #[serde(default, skip_serializing_if = "Option::is_none")]
101        filter: Option<Box<Self>>,
102        #[serde(default, skip_serializing_if = "WindowCallModifiers::is_empty")]
103        modifiers: WindowCallModifiers,
104    },
105    Case {
106        base: Option<Box<Self>>,
107        when: Vec<(Self, Self)>,
108        else_branch: Option<Box<Self>>,
109    },
110    Cast {
111        /// Analysis introduced this coercion rather than retaining an explicit SQL cast. Stored definitions must distinguish array coercions from explicit constructor conversions.
112        #[serde(default, skip_serializing_if = "is_false")]
113        implicit: bool,
114        expr: Box<Self>,
115        ty: String,
116    },
117    ScalarSubquery(SubqueryId),
118    Exists {
119        subquery: SubqueryId,
120        negated: bool,
121    },
122    InSubquery {
123        expr: Box<Self>,
124        subquery: SubqueryId,
125        negated: bool,
126    },
127}
128
129#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
130pub struct ScalarOrder {
131    pub expr: ScalarExpr,
132    pub descending: bool,
133    pub nulls: Option<NullsOrder>,
134}
135
136#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
137pub struct ScalarWindowSpec {
138    /// Canonical definition in the enclosing query block; absent in legacy inline plans.
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub definition: Option<usize>,
141    pub partition_by: Vec<ScalarExpr>,
142    pub order_by: Vec<ScalarOrder>,
143    pub frame: Option<ScalarWindowFrame>,
144}
145
146impl ScalarWindowSpec {
147    /// Borrow each key and frame-offset root owned by this specification.
148    pub fn expressions(&self) -> impl Iterator<Item = &ScalarExpr> {
149        self.partition_by
150            .iter()
151            .chain(self.order_by.iter().map(|order| &order.expr))
152            .chain(
153                self.frame
154                    .iter()
155                    .flat_map(|frame| [&frame.start, &frame.end])
156                    .filter_map(|bound| match bound {
157                        ScalarFrameBound::Preceding(value) | ScalarFrameBound::Following(value) => {
158                            Some(value.as_ref())
159                        }
160                        _ => None,
161                    }),
162            )
163    }
164
165    /// Mutably borrow each key and frame-offset root without visiting derived call copies.
166    pub fn expressions_mut(&mut self) -> impl Iterator<Item = &mut ScalarExpr> {
167        self.partition_by
168            .iter_mut()
169            .chain(self.order_by.iter_mut().map(|order| &mut order.expr))
170            .chain(
171                self.frame
172                    .iter_mut()
173                    .flat_map(|frame| [&mut frame.start, &mut frame.end])
174                    .filter_map(|bound| match bound {
175                        ScalarFrameBound::Preceding(value) | ScalarFrameBound::Following(value) => {
176                            Some(value.as_mut())
177                        }
178                        _ => None,
179                    }),
180            )
181    }
182}
183
184#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
185pub struct ScalarWindowFrame {
186    pub mode: FrameMode,
187    pub start: ScalarFrameBound,
188    pub end: ScalarFrameBound,
189    /// Whether the frame was written `BETWEEN start AND end`; see [`crate::ast::WindowFrame::between`].
190    #[serde(default = "frame_written_between")]
191    pub between: bool,
192    #[serde(default, skip_serializing_if = "FrameExclusion::is_no_others")]
193    pub exclusion: FrameExclusion,
194}
195
196/// Frames recorded before the spelling was kept were deparsed with `BETWEEN`.
197const fn frame_written_between() -> bool {
198    true
199}
200
201#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
202pub enum ScalarFrameBound {
203    UnboundedPreceding,
204    UnboundedFollowing,
205    CurrentRow,
206    Preceding(Box<ScalarExpr>),
207    Following(Box<ScalarExpr>),
208}
209
210impl ScalarExpr {
211    #[must_use]
212    pub fn qualified_column(qualifier: impl Into<String>, column: impl Into<String>) -> Self {
213        Self::QualifiedColumn {
214            qualifier: qualifier.into(),
215            column: column.into(),
216        }
217    }
218}
219
220#[expect(
221    clippy::trivially_copy_pass_by_ref,
222    reason = "serde skip_serializing_if requires a borrowed field"
223)]
224const fn is_false(value: &bool) -> bool {
225    !*value
226}
227
228#[cfg(test)]
229mod tests {
230    use super::ScalarExpr;
231
232    #[test]
233    fn syntax_cast_origin_survives_owned_and_borrowed_lowering() {
234        use crate::{ast::Expr, plan::ExpressionPlan};
235        use uqa_core::{memory::MemoryBudget, CancellationToken};
236
237        let legacy = r#"{"Cast":{"expr":{"Column":"value"},"ty":"bigint[]"}}"#;
238        let explicit: Expr = serde_json::from_str(legacy).unwrap();
239        assert!(matches!(
240            explicit,
241            Expr::Cast {
242                implicit: false,
243                ..
244            }
245        ));
246        assert_eq!(serde_json::to_string(&explicit).unwrap(), legacy);
247        let mut implicit = explicit;
248        let Expr::Cast {
249            implicit: origin, ..
250        } = &mut implicit
251        else {
252            unreachable!()
253        };
254        *origin = true;
255        let stored = serde_json::to_string(&implicit).unwrap();
256        let restored: Expr = serde_json::from_str(&stored).unwrap();
257        let expected = ExpressionPlan::lower(restored.clone()).scalar;
258        assert!(matches!(expected, ScalarExpr::Cast { implicit: true, .. }));
259        let budget = MemoryBudget::new(1 << 20);
260        let token = CancellationToken::new();
261        let borrowed =
262            ExpressionPlan::lower_column_budgeted(&restored, &budget, &token, &token).unwrap();
263        assert_eq!(*borrowed, expected);
264    }
265
266    #[test]
267    fn cast_origin_survives_storage_and_legacy_casts_remain_explicit() {
268        let legacy = r#"{"Cast":{"expr":{"Column":"value"},"ty":"bigint[]"}}"#;
269        let explicit: ScalarExpr = serde_json::from_str(legacy).unwrap();
270        assert!(matches!(
271            explicit,
272            ScalarExpr::Cast {
273                implicit: false,
274                ..
275            }
276        ));
277        assert_eq!(serde_json::to_string(&explicit).unwrap(), legacy);
278        let mut implicit = explicit.clone();
279        let ScalarExpr::Cast {
280            implicit: origin, ..
281        } = &mut implicit
282        else {
283            unreachable!()
284        };
285        *origin = true;
286        let stored = serde_json::to_string(&implicit).unwrap();
287        assert_eq!(
288            serde_json::from_str::<ScalarExpr>(&stored).unwrap(),
289            implicit
290        );
291        assert_ne!(implicit, explicit);
292    }
293}