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