Skip to main content

uqa_planner/unified_plan/
model.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Serializable relational, command, source, and scalar plan data model.
8
9use super::{NullsOrder, ScalarExpr, SetOpKind};
10
11const fn default_include_descendants() -> bool {
12    true
13}
14
15/// One fully lowered SQL statement.
16///
17/// There is deliberately no `Legacy`, `Opaque`, or raw-`Statement` variant:
18/// adding a SQL statement kind must update the exhaustive lowerer and the
19/// physical driver.
20#[derive(Debug, Clone)]
21pub enum UnifiedPlan {
22    Query(Box<QueryPlan>),
23    Command(Box<CommandPlan>),
24}
25
26/// A relational query with its CTE scope and one relational root.
27#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
28pub struct QueryPlan {
29    pub ctes: Vec<CtePlan>,
30    pub root: RelationalPlan,
31}
32
33/// A named query child owned by a [`QueryPlan`].
34#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub struct CtePlan {
36    pub name: String,
37    pub columns: Vec<String>,
38    pub recursive: bool,
39    #[serde(default)]
40    pub materialization: uqa_sql::ast::CteMaterialization,
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub search: Option<CteSearchPlan>,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub cycle: Option<CteCyclePlan>,
45    pub query: Box<QueryPlan>,
46}
47
48/// Generated traversal-order column for a recursive CTE.
49#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
50pub struct CteSearchPlan {
51    pub columns: Vec<String>,
52    pub breadth_first: bool,
53    pub sequence_column: String,
54}
55
56/// Generated cycle mark and path columns for a recursive CTE.
57#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
58pub struct CteCyclePlan {
59    pub columns: Vec<String>,
60    pub mark_column: String,
61    pub mark_value: ScalarExpr,
62    pub mark_default: ScalarExpr,
63    pub path_column: String,
64}
65
66/// Relational nodes common to ordinary SQL, retrieval SQL, and table/graph
67/// functions.
68#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
69pub enum RelationalPlan {
70    /// A single SELECT query block. Its source is a separate plan tree and its
71    /// compute phase is classified as projection, aggregation, or windowing.
72    QueryBlock(Box<QueryBlockPlan>),
73    /// SQL set operations own both input plans; combined ordering and slicing
74    /// are properties of the set node rather than either branch.
75    SetOp {
76        kind: SetOpKind,
77        all: bool,
78        left: Box<QueryPlan>,
79        right: Box<QueryPlan>,
80        order_by: Vec<OrderPlan>,
81        limit: Option<Box<ScalarExpr>>,
82        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
83        with_ties: bool,
84        offset: Option<Box<ScalarExpr>>,
85        subqueries: Vec<QueryPlan>,
86    },
87    /// Standalone `VALUES`, used both as a statement and as a relational
88    /// source. Each cell remains an expression so parameters/functions bind at
89    /// execution time.
90    Values {
91        rows: Vec<Vec<ScalarExpr>>,
92        subqueries: Vec<QueryPlan>,
93    },
94}
95
96/// One SELECT block after `WITH` and set-operation structure has been pulled
97/// into explicit parent/child nodes.
98#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
99pub struct QueryBlockPlan {
100    pub projections: Vec<ProjectionPlan>,
101    pub from: Option<SourcePlan>,
102    pub r#where: Option<ScalarExpr>,
103    pub compute: ComputePlan,
104    pub group_by: Vec<ScalarExpr>,
105    pub grouping_sets: Vec<Vec<ScalarExpr>>,
106    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
107    pub group_distinct: bool,
108    pub having: Option<ScalarExpr>,
109    pub order_by: Vec<OrderPlan>,
110    pub limit: Option<ScalarExpr>,
111    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
112    pub with_ties: bool,
113    pub offset: Option<ScalarExpr>,
114    pub distinct: bool,
115    pub distinct_on: Vec<ScalarExpr>,
116    pub subqueries: Vec<QueryPlan>,
117    pub access: AccessPathPlan,
118    /// `FOR UPDATE` / `FOR SHARE` clauses belonging to this query block.
119    #[serde(default, skip_serializing_if = "Vec::is_empty")]
120    pub locking: Vec<uqa_sql::ast::LockingClause>,
121}
122
123/// Cross-paradigm access decision made after the relational and scalar
124/// portions of a query block have both been lowered.
125#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
126pub enum AccessPathPlan {
127    /// Ordinary row-source execution.
128    Row,
129    /// Use the shared document-support/operator algebra for the block predicate.
130    OperatorTree {
131        /// The relational ORDER BY/OFFSET/LIMIT can be pushed into the
132        /// retrieval function before row materialization.
133        score_limit_pushdown: bool,
134    },
135    /// Split a mixed predicate into posting-list candidates followed by
136    /// row-level residual evaluation.
137    Hybrid,
138}
139
140/// Physical strategy selected for a relational join.
141///
142/// `Auto` is used for an unreordered SQL join and lets physical lowering pick
143/// hash execution for a splittable equality predicate or nested-loop execution
144/// otherwise. `Hash` is an optimizer commitment produced by DPccp and must be
145/// executable; physical lowering reports an internal planning error if that
146/// invariant is violated.
147#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
148pub enum JoinExecutionStrategy {
149    #[default]
150    Auto,
151    Hash,
152}
153
154/// One independently resolved and bound function inside a range-function group.
155#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
156pub struct TableFunctionPlan {
157    pub name: String,
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub binding: Option<uqa_sql::ast::FunctionBinding>,
160    #[serde(default)]
161    pub output_name: String,
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub relation: Option<String>,
164    pub args: Vec<ScalarExpr>,
165    pub column_aliases: Vec<String>,
166    pub column_types: Vec<String>,
167}
168
169/// The row-producing source below a query block.
170#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
171pub enum SourcePlan {
172    Table {
173        name: String,
174        #[serde(default)]
175        qualifier: String,
176        alias: Option<String>,
177        #[serde(default = "default_include_descendants")]
178        include_descendants: bool,
179    },
180    Join {
181        left: Box<SourcePlan>,
182        right: Box<SourcePlan>,
183        kind: uqa_sql::ast::JoinKind,
184        on: Option<ScalarExpr>,
185        #[serde(default, skip_serializing_if = "Option::is_none")]
186        using: Option<uqa_sql::ast::JoinUsing>,
187        #[serde(default)]
188        natural: bool,
189        #[serde(default, skip_serializing_if = "Option::is_none")]
190        alias: Option<String>,
191        #[serde(default, skip_serializing_if = "Vec::is_empty")]
192        column_aliases: Vec<String>,
193        lateral: bool,
194        #[serde(default)]
195        strategy: JoinExecutionStrategy,
196    },
197    Values {
198        rows: Vec<Vec<ScalarExpr>>,
199        alias: Option<String>,
200        column_aliases: Vec<String>,
201        #[serde(default, skip_serializing_if = "Option::is_none")]
202        internal_relation: Option<uqa_sql::ast::InternalRelationId>,
203        #[serde(default, skip_serializing_if = "Vec::is_empty")]
204        internal_column_types: Vec<Option<uqa_sql::ast::ColumnType>>,
205    },
206    Function {
207        name: String,
208        #[serde(default, skip_serializing_if = "Option::is_none")]
209        binding: Option<uqa_sql::ast::FunctionBinding>,
210        #[serde(default)]
211        output_name: String,
212        #[serde(default, skip_serializing_if = "Option::is_none")]
213        relation: Option<String>,
214        args: Vec<ScalarExpr>,
215        alias: Option<String>,
216        column_aliases: Vec<String>,
217        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
218        ordinality: bool,
219        column_types: Vec<String>,
220    },
221    FunctionGroup {
222        functions: Vec<TableFunctionPlan>,
223        alias: Option<String>,
224        column_aliases: Vec<String>,
225        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
226        ordinality: bool,
227    },
228    Subquery {
229        body: Box<QueryPlan>,
230        alias: Option<String>,
231        column_aliases: Vec<String>,
232    },
233}
234
235/// The SELECT-list phase chosen during lowering.
236#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
237pub enum ComputePlan {
238    Project,
239    Aggregate,
240    Window,
241}
242
243#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
244pub struct ProjectionPlan {
245    pub expr: ScalarExpr,
246    pub alias: Option<String>,
247}
248
249#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
250pub struct OrderPlan {
251    pub expr: ScalarExpr,
252    pub descending: bool,
253    pub nulls: Option<NullsOrder>,
254}
255
256/// Executable scalar IR plus every query-valued descendant it owns.
257#[derive(Debug, Clone)]
258pub struct ExpressionPlan {
259    pub scalar: ScalarExpr,
260    pub subqueries: Vec<QueryPlan>,
261}
262
263#[derive(Debug, Clone)]
264pub struct AssignmentPlan {
265    pub column: String,
266    pub value: ScalarExpr,
267}
268
269#[derive(Debug, Clone)]
270pub struct InsertPlan {
271    pub table: String,
272    pub target_qualifier: String,
273    pub include_descendants: bool,
274    pub columns: Vec<String>,
275    pub ctes: Vec<CtePlan>,
276    pub rows: Vec<Vec<ScalarExpr>>,
277    pub source: Option<Box<QueryPlan>>,
278    pub on_conflict: Option<ConflictPlan>,
279    pub returning: Vec<ProjectionPlan>,
280    pub returning_aliases: uqa_sql::ast::ReturningAliases,
281    pub subqueries: Vec<QueryPlan>,
282}
283
284#[derive(Debug, Clone)]
285pub struct ConflictPlan {
286    pub conflict_columns: Vec<String>,
287    pub action: ConflictActionPlan,
288}
289
290#[derive(Debug, Clone)]
291pub enum ConflictActionPlan {
292    Nothing,
293    Update {
294        assignments: Vec<AssignmentPlan>,
295        predicate: Option<ScalarExpr>,
296    },
297}
298
299#[derive(Debug, Clone)]
300pub struct UpdatePlan {
301    pub table: String,
302    pub target_qualifier: String,
303    pub include_descendants: bool,
304    pub assignments: Vec<AssignmentPlan>,
305    pub predicate: Option<ScalarExpr>,
306    pub ctes: Vec<CtePlan>,
307    pub source: Option<Box<SourcePlan>>,
308    pub returning: Vec<ProjectionPlan>,
309    pub returning_aliases: uqa_sql::ast::ReturningAliases,
310    pub subqueries: Vec<QueryPlan>,
311}
312
313#[derive(Debug, Clone)]
314pub struct DeletePlan {
315    pub table: String,
316    pub target_qualifier: String,
317    pub include_descendants: bool,
318    pub predicate: Option<ScalarExpr>,
319    pub ctes: Vec<CtePlan>,
320    pub source: Option<Box<SourcePlan>>,
321    pub returning: Vec<ProjectionPlan>,
322    pub returning_aliases: uqa_sql::ast::ReturningAliases,
323    pub subqueries: Vec<QueryPlan>,
324}
325
326#[derive(Debug, Clone)]
327pub struct MergePlan {
328    pub target: String,
329    pub target_qualifier: String,
330    pub target_alias: Option<String>,
331    pub include_descendants: bool,
332    pub source: Box<SourcePlan>,
333    pub join_condition: ScalarExpr,
334    pub when_clauses: Vec<MergeWhenPlan>,
335    pub returning: Vec<ProjectionPlan>,
336    pub returning_aliases: uqa_sql::ast::ReturningAliases,
337    pub subqueries: Vec<QueryPlan>,
338}
339
340#[derive(Debug, Clone)]
341pub enum MergeWhenPlan {
342    UpdateMatched {
343        condition: Option<ScalarExpr>,
344        assignments: Vec<AssignmentPlan>,
345    },
346    DeleteMatched {
347        condition: Option<ScalarExpr>,
348    },
349    UpdateNotMatchedBySource {
350        condition: Option<ScalarExpr>,
351        assignments: Vec<AssignmentPlan>,
352    },
353    DeleteNotMatchedBySource {
354        condition: Option<ScalarExpr>,
355    },
356    InsertNotMatched {
357        condition: Option<ScalarExpr>,
358        columns: Vec<String>,
359        values: Vec<ScalarExpr>,
360    },
361    NothingMatched {
362        condition: Option<ScalarExpr>,
363    },
364    NothingNotMatched {
365        condition: Option<ScalarExpr>,
366    },
367    NothingNotMatchedBySource {
368        condition: Option<ScalarExpr>,
369    },
370}
371
372/// Non-query statement plans. Mutations own physical sources and scalar IR;
373/// query-bearing catalog commands own explicit query children. Typed DDL and
374/// procedural payloads contain catalog data, never a second SQL dispatcher.
375#[derive(Debug, Clone)]
376pub enum CommandPlan {
377    CreateTable(Box<uqa_sql::ast::CreateTable>),
378    CreateIndex(uqa_sql::ast::CreateIndex),
379    Insert(Box<InsertPlan>),
380    Update(Box<UpdatePlan>),
381    Delete(Box<DeletePlan>),
382    Drop(uqa_sql::ast::DropStmt),
383    AlterTable(Box<uqa_sql::ast::AlterTableStmt>),
384    AlterViewOptions(uqa_sql::ast::AlterViewOptionsStmt),
385    CreateView {
386        name: String,
387        column_names: Vec<String>,
388        query: Box<QueryPlan>,
389        or_replace: bool,
390        persistence: uqa_sql::ast::RelationPersistence,
391        options: Vec<(String, String)>,
392    },
393    CreateMaterializedView {
394        name: String,
395        column_names: Vec<String>,
396        if_not_exists: bool,
397        with_no_data: bool,
398        options: Vec<(String, String)>,
399        query: Box<QueryPlan>,
400    },
401    RefreshMaterializedView {
402        name: String,
403        concurrently: bool,
404        with_no_data: bool,
405    },
406    CreateSchema {
407        name: String,
408        if_not_exists: bool,
409    },
410    SetVariable {
411        name: String,
412        value: String,
413    },
414    ResetVariable {
415        name: String,
416    },
417    ResetAllVariables,
418    SetConstraints {
419        constraints: Vec<uqa_sql::ast::SetConstraintName>,
420        deferred: bool,
421    },
422    ShowVariable {
423        name: String,
424    },
425    Discard {
426        target: uqa_sql::ast::DiscardTarget,
427    },
428    Load {
429        library: String,
430    },
431    Explain {
432        analyze: bool,
433        verbose: bool,
434        format: Option<String>,
435        body: Box<UnifiedPlan>,
436    },
437    Analyze {
438        table: Option<String>,
439    },
440    Vacuum(uqa_sql::ast::VacuumStmt),
441    Truncate {
442        tables: Vec<uqa_sql::ast::TruncateTarget>,
443        cascade: bool,
444        restart_identity: bool,
445    },
446    Transaction(uqa_sql::ast::TransactionStmt),
447    DeclareCursor {
448        name: String,
449        binary: bool,
450        scroll: Option<bool>,
451        hold: bool,
452        query: Box<QueryPlan>,
453    },
454    FetchCursor(uqa_sql::ast::FetchCursorStmt),
455    CloseCursor {
456        name: Option<String>,
457    },
458    CreateSequence(uqa_sql::ast::CreateSequence),
459    AlterSequence(uqa_sql::ast::AlterSequence),
460    CreateTableAs {
461        name: String,
462        if_not_exists: bool,
463        column_names: Vec<String>,
464        with_no_data: bool,
465        persistence: uqa_sql::ast::RelationPersistence,
466        on_commit: uqa_sql::ast::OnCommitAction,
467        query: Box<QueryPlan>,
468    },
469    Prepare {
470        name: String,
471        body: Box<UnifiedPlan>,
472    },
473    Execute {
474        name: String,
475        params: Vec<ExpressionPlan>,
476    },
477    Deallocate {
478        name: Option<String>,
479    },
480    CreateForeignServer(uqa_sql::ast::CreateForeignServer),
481    CreateForeignTable(uqa_sql::ast::CreateForeignTable),
482    Merge(Box<MergePlan>),
483    CreateFunction(Box<uqa_sql::ast::CreateFunction>),
484    DropFunction(uqa_sql::ast::DropFunctionStmt),
485    AlterRoutine(uqa_sql::ast::AlterRoutineStmt),
486    AlterRoutineOwner(uqa_sql::ast::AlterRoutineOwnerStmt),
487    GrantRoutine(uqa_sql::ast::GrantRoutineStmt),
488    CreateRole(uqa_sql::ast::CreateRoleStmt),
489    AlterRole(uqa_sql::ast::AlterRoleStmt),
490    DropRole(uqa_sql::ast::DropRoleStmt),
491    CreateTrigger(uqa_sql::ast::CreateTrigger),
492    DropTrigger(uqa_sql::ast::DropTrigger),
493    CreateRule(uqa_sql::ast::CreateRule),
494    DropRule(uqa_sql::ast::DropRule),
495    DoBlock {
496        language: String,
497        body: String,
498    },
499    Call {
500        name: String,
501        args: Vec<ExpressionPlan>,
502    },
503}
504
505/// Classification hook for engine-registered aggregate functions. Built-in
506/// aggregates are always recognised; the callback extends that set without
507/// making the planner depend on the engine.
508pub trait AggregateClassifier {
509    fn is_registered_aggregate(&self, name: &str) -> bool;
510}
511
512impl<F> AggregateClassifier for F
513where
514    F: Fn(&str) -> bool,
515{
516    fn is_registered_aggregate(&self, name: &str) -> bool {
517        self(name)
518    }
519}
520
521pub(super) struct NoRegisteredAggregates;
522
523impl AggregateClassifier for NoRegisteredAggregates {
524    fn is_registered_aggregate(&self, _name: &str) -> bool {
525        false
526    }
527}