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