Skip to main content

uqa_planner/statement_planning/
rule_inputs.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Prune mutation inputs discarded by rewrite rules before constant evaluation.
8use crate::{
9    CommandPlan, CtePlan, CtePlanBody, QueryPlan, RelationalPlan, SourcePlan, UnifiedPlan,
10};
11use std::collections::BTreeSet;
12use uqa_core::Value;
13use uqa_sql::{
14    ast::RuleEvent,
15    semantics::{rules::RuleCatalog, view_rewrite::context::ViewRewriteContext},
16    SQLError, ScalarExpr,
17};
18
19pub trait RuleInputColumns {
20    fn source_column_names(
21        &self,
22        table: &str,
23        relations_bound: bool,
24    ) -> Result<Option<Vec<String>>, SQLError>;
25}
26pub struct RuleInputPlanningContext<'a> {
27    pub rules: &'a dyn RuleCatalog,
28    pub views: ViewRewriteContext<'a>,
29    pub columns: &'a dyn RuleInputColumns,
30}
31pub fn rewrite_plan(
32    context: &RuleInputPlanningContext<'_>,
33    plan: &mut UnifiedPlan,
34) -> Result<(), SQLError> {
35    match plan {
36        UnifiedPlan::Query(query) => rewrite_query(context, query),
37        UnifiedPlan::Command(command) => rewrite_command(context, command),
38    }
39}
40
41fn rewrite_ctes(
42    context: &RuleInputPlanningContext<'_>,
43    ctes: &mut [CtePlan],
44) -> Result<(), SQLError> {
45    for cte in ctes {
46        match &mut cte.body {
47            CtePlanBody::Query(query) => rewrite_query(context, query)?,
48            CtePlanBody::Command(command) => rewrite_command(context, command)?,
49        }
50    }
51    Ok(())
52}
53
54fn rewrite_query(
55    context: &RuleInputPlanningContext<'_>,
56    query: &mut QueryPlan,
57) -> Result<(), SQLError> {
58    rewrite_ctes(context, &mut query.ctes)?;
59    let subqueries = match &mut query.root {
60        RelationalPlan::QueryBlock(block) => {
61            if let Some(source) = &mut block.from {
62                rewrite_source(context, source)?;
63            }
64            &mut block.subqueries
65        }
66        RelationalPlan::Values { subqueries, .. } => subqueries,
67        RelationalPlan::SetOp {
68            left,
69            right,
70            subqueries,
71            ..
72        } => {
73            rewrite_query(context, left)?;
74            rewrite_query(context, right)?;
75            subqueries
76        }
77    };
78    for query in subqueries {
79        rewrite_query(context, query)?;
80    }
81    Ok(())
82}
83
84fn rewrite_source(
85    context: &RuleInputPlanningContext<'_>,
86    source: &mut SourcePlan,
87) -> Result<(), SQLError> {
88    match source {
89        SourcePlan::Subquery { body, .. } => rewrite_query(context, body)?,
90        SourcePlan::Join { left, right, .. } => {
91            rewrite_source(context, left)?;
92            rewrite_source(context, right)?;
93        }
94        _ => {}
95    }
96    Ok(())
97}
98
99fn rewrite_command(
100    context: &RuleInputPlanningContext<'_>,
101    command: &mut CommandPlan,
102) -> Result<(), SQLError> {
103    match command {
104        CommandPlan::Explain { body, .. } => return rewrite_plan(context, body),
105        CommandPlan::DeclareCursor { query, .. } => return rewrite_query(context, query),
106        _ => {}
107    }
108    prune_rule_inputs(context, command)?;
109    if let Some(ctes) = command.ctes_mut() {
110        rewrite_ctes(context, ctes)?;
111    }
112    if let Some(source) = command.source_input_mut() {
113        rewrite_source(context, source)?;
114    }
115    for query in command.query_inputs_mut() {
116        rewrite_query(context, query)?;
117    }
118    Ok(())
119}
120
121fn prune_rule_inputs(
122    context: &RuleInputPlanningContext<'_>,
123    command: &mut CommandPlan,
124) -> Result<(), SQLError> {
125    let (table, bound, event) = match command {
126        CommandPlan::Insert(plan) => (&plan.table, plan.target_relation_bound, RuleEvent::Insert),
127        CommandPlan::Update(plan) => (&plan.table, plan.target_relation_bound, RuleEvent::Update),
128        CommandPlan::Delete(plan) => (&plan.table, plan.target_relation_bound, RuleEvent::Delete),
129        _ => return Ok(()),
130    };
131    let table = context.rules.resolve_mutation_target(table, bound)?;
132    if let CommandPlan::Update(plan) = command {
133        uqa_sql::semantics::view_rewrite::validate_grouped_rule_inputs(
134            context.views,
135            &table,
136            &plan.assignments,
137        )?;
138    }
139    let Some(requirements) =
140        uqa_sql::semantics::view_rewrite::rule_input_requirements(context.views, &table, event)?
141    else {
142        return Ok(());
143    };
144    uqa_sql::semantics::rules::validate_rule_returning_contract(
145        context.rules,
146        &table,
147        event,
148        command
149            .returning()
150            .is_some_and(|returning| !returning.is_empty()),
151    )?;
152    let requires_rows = requirements.requires_rows;
153    let required = requirements.columns;
154    match command {
155        CommandPlan::Insert(plan) => {
156            let columns = if plan.columns.is_empty() {
157                context
158                    .columns
159                    .source_column_names(&table, true)?
160                    .unwrap_or_default()
161            } else {
162                plan.columns
163                    .iter()
164                    .map(|target| target.column.clone())
165                    .collect()
166            };
167            let positions = columns
168                .iter()
169                .enumerate()
170                .filter_map(|(position, column)| required.contains(column).then_some(position))
171                .collect::<BTreeSet<_>>();
172            for (position, target) in plan.columns.iter_mut().enumerate() {
173                if !positions.contains(&position) {
174                    for expression in target.expressions_mut() {
175                        *expression = ScalarExpr::Literal(Value::Null);
176                    }
177                }
178            }
179            for row in &mut plan.rows {
180                for (position, expression) in row.iter_mut().enumerate() {
181                    if !positions.contains(&position) {
182                        *expression = ScalarExpr::Literal(Value::Null);
183                    }
184                }
185            }
186            if !requires_rows {
187                plan.source = None;
188                plan.ctes.clear();
189                plan.subqueries.clear();
190            } else if let Some(source) = &mut plan.source {
191                crate::mutation_outputs::prune_unused_query_outputs(
192                    source,
193                    &positions,
194                    columns.len(),
195                );
196            }
197        }
198        CommandPlan::Update(plan) => {
199            prune_update_assignments(&mut plan.assignments, &required);
200            if !requires_rows {
201                plan.source = None;
202                plan.predicate = None;
203                plan.ctes.clear();
204                plan.subqueries.clear();
205            }
206        }
207        CommandPlan::Delete(plan) if !requires_rows => {
208            plan.source = None;
209            plan.predicate = None;
210            plan.ctes.clear();
211            plan.subqueries.clear();
212        }
213        _ => {}
214    }
215    Ok(())
216}
217
218fn prune_update_assignments(
219    assignments: &mut Vec<uqa_sql::plan::AssignmentPlan>,
220    required: &BTreeSet<String>,
221) {
222    *assignments = std::mem::take(assignments)
223        .into_iter()
224        .flat_map(|assignment| {
225            if assignment
226                .target
227                .column_names()
228                .all(|column| !required.contains(column))
229            {
230                assignment
231                    .target
232                    .targets()
233                    .iter()
234                    .cloned()
235                    .map(|target| uqa_sql::plan::AssignmentPlan {
236                        target: target.into(),
237                        value: ScalarExpr::Literal(Value::Null),
238                    })
239                    .collect::<Vec<_>>()
240            } else {
241                vec![assignment]
242            }
243        })
244        .collect();
245    for assignment in assignments {
246        if assignment
247            .target
248            .column_names()
249            .all(|column| !required.contains(column))
250        {
251            for expression in assignment.expressions_mut() {
252                *expression = ScalarExpr::Literal(Value::Null);
253            }
254        }
255    }
256}