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