Skip to main content

radixdb_executor/mutation/
host.rs

1//! Internal mutation port used by bounded executor modules.
2//!
3//! Mutation implementations own their bind/validate/execute/finalize phases
4//! here. The host supplies shared executor state and calls back into the single
5//! SELECT owner without duplicating that owner in every mutation module.
6
7use std::sync::{Arc, Mutex};
8
9use radixdb_catalog::{CatalogMutationSet, TriggerTiming};
10use radixdb_core::{DataType, IsolationLevel, Result, Row, RowVec};
11use radixdb_functions::FunctionRegistry;
12use radixdb_plugin_host::PluginRegistry;
13use radixdb_sql::ast::{
14    CreateJobStatement, CreateRoutineStatement, CreateTriggerStatement, Expression,
15    SelectStatement, Statement,
16};
17use radixdb_storage::mvcc::engine::MVCCEngine;
18use radixdb_storage::traits::{QueryResult, Table, Transaction};
19use rustc_hash::FxHashMap;
20
21use crate::catalog::DdlTransaction;
22use crate::context::ExecutionContext;
23use crate::procedural::{CallBoundary, DmlTriggerEvent, DmlTriggerPlan};
24
25/// Explicit transaction state shared by statement routing and mutation owners.
26#[doc(hidden)]
27pub struct ActiveTransaction {
28    pub transaction: Box<dyn Transaction>,
29    pub tables: FxHashMap<String, Box<dyn Table>>,
30    pub catalog: DdlTransaction,
31    catalog_savepoints: FxHashMap<String, DdlTransaction>,
32}
33
34impl ActiveTransaction {
35    pub fn new(transaction: Box<dyn Transaction>, catalog: DdlTransaction) -> Self {
36        Self {
37            transaction,
38            tables: FxHashMap::default(),
39            catalog,
40            catalog_savepoints: FxHashMap::default(),
41        }
42    }
43
44    pub fn create_savepoint(&mut self, name: &str) -> Result<()> {
45        self.transaction.create_savepoint(name)?;
46        self.catalog_savepoints
47            .insert(name.to_owned(), self.catalog.clone());
48        Ok(())
49    }
50
51    pub fn release_savepoint(&mut self, name: &str) -> Result<()> {
52        self.transaction.release_savepoint(name)?;
53        self.catalog_savepoints.remove(name);
54        Ok(())
55    }
56
57    pub fn rollback_to_savepoint(&mut self, name: &str) -> Result<()> {
58        let catalog = self.catalog_savepoints.get(name).cloned().ok_or_else(|| {
59            radixdb_core::Error::invalid_argument(format!(
60                "savepoint '{name}' has no catalog snapshot"
61            ))
62        })?;
63        let timestamp = self
64            .transaction
65            .get_savepoint_timestamp(name)
66            .ok_or_else(|| {
67                radixdb_core::Error::invalid_argument(format!(
68                    "savepoint '{name}' has no storage timestamp"
69                ))
70            })?;
71        for table in self.tables.values() {
72            table.rollback_to_timestamp(timestamp);
73        }
74        self.transaction.rollback_to_savepoint(name)?;
75        self.catalog = catalog;
76        Ok(())
77    }
78
79    pub fn rollback(&mut self) -> Result<()> {
80        // The storage transaction must classify the unit before table handles
81        // discard their private versions. Otherwise a real DML rollback looks
82        // read-only, no abort marker reaches WAL, and reopen may reuse its ID.
83        let result = self.transaction.rollback();
84        for (_, mut table) in self.tables.drain() {
85            table.rollback();
86        }
87        result
88    }
89
90    pub fn stage_catalog_for_commit(&mut self) -> Result<()> {
91        if let Some(mutation) = self.catalog.pending_mutation()? {
92            self.transaction.stage_catalog_mutation(mutation)?;
93        }
94        Ok(())
95    }
96
97    pub fn has_pending_catalog_changes(&self) -> bool {
98        self.catalog.has_pending_catalog_changes()
99    }
100
101    pub fn stage_catalog_statement(&mut self, statement: Statement) -> Result<()> {
102        self.catalog.stage_statement(statement)
103    }
104}
105
106/// Neutral SELECT output metadata consumed by CTAS binding.
107#[doc(hidden)]
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct BoundQueryColumn {
110    pub name: String,
111    pub data_type: DataType,
112    pub nullable: bool,
113}
114
115/// Narrow callbacks supplied by the concrete executor composition owner.
116#[doc(hidden)]
117pub trait MutationHost {
118    fn mutation_engine(&self) -> &Arc<MVCCEngine>;
119
120    fn mutation_default_isolation_level(&self) -> IsolationLevel;
121
122    fn mutation_function_registry(&self) -> &Arc<FunctionRegistry>;
123
124    fn mutation_plugin_registry(&self) -> &Arc<PluginRegistry>;
125
126    fn mutation_active_transaction(&self) -> &Mutex<Option<ActiveTransaction>>;
127
128    fn mutation_execute_select(
129        &self,
130        statement: &SelectStatement,
131        ctx: &ExecutionContext,
132    ) -> Result<Box<dyn QueryResult>>;
133
134    fn mutation_describe_select_output(
135        &self,
136        statement: &SelectStatement,
137    ) -> Result<Vec<BoundQueryColumn>>;
138
139    fn mutation_compile_routine(
140        &self,
141        statement: &CreateRoutineStatement,
142        catalog: &radixdb_catalog::CatalogGeneration,
143        identity: radixdb_procedural::CompileIdentity,
144        search_path: Vec<radixdb_catalog::ObjectId>,
145    ) -> Result<Vec<radixdb_catalog::ObjectId>>;
146
147    fn mutation_validate_trigger(
148        &self,
149        statement: &CreateTriggerStatement,
150        catalog: &radixdb_catalog::CatalogGeneration,
151    ) -> Result<Vec<radixdb_catalog::ObjectId>>;
152
153    fn mutation_bind_job(
154        &self,
155        statement: &CreateJobStatement,
156        catalog: &radixdb_catalog::CatalogGeneration,
157        context: &ExecutionContext,
158    ) -> Result<crate::catalog::BoundJobDefinition>;
159
160    fn mutation_prepare_dml_triggers(
161        &self,
162        table_name: &str,
163        event: DmlTriggerEvent,
164        updated_columns: &[String],
165        context: &ExecutionContext,
166    ) -> Result<DmlTriggerPlan>;
167
168    fn mutation_begin_trigger_boundary(&self) -> Result<CallBoundary>;
169
170    fn mutation_complete_trigger_boundary(&self, boundary: &CallBoundary) -> Result<()>;
171
172    fn mutation_abort_trigger_boundary(&self, boundary: &CallBoundary) -> Result<()>;
173
174    fn mutation_fire_statement_triggers(
175        &self,
176        plan: &DmlTriggerPlan,
177        timing: TriggerTiming,
178        context: &ExecutionContext,
179    ) -> Result<()>;
180
181    fn mutation_fire_before_row_triggers(
182        &self,
183        plan: &DmlTriggerPlan,
184        old: Option<&Row>,
185        new: Option<Row>,
186        row_identity: Option<i64>,
187        context: &ExecutionContext,
188    ) -> Result<Option<Row>>;
189
190    fn mutation_fire_after_row_triggers(
191        &self,
192        plan: &DmlTriggerPlan,
193        old: Option<&Row>,
194        new: Option<&Row>,
195        row_identity: Option<i64>,
196        context: &ExecutionContext,
197    ) -> Result<()>;
198
199    fn mutation_process_where_subqueries(
200        &self,
201        expression: &Expression,
202        ctx: &ExecutionContext,
203    ) -> Result<Expression>;
204
205    fn mutation_process_correlated_expression(
206        &self,
207        expression: &Expression,
208        ctx: &ExecutionContext,
209    ) -> Result<Expression>;
210
211    fn mutation_process_correlated_where(
212        &self,
213        expression: &Expression,
214        ctx: &ExecutionContext,
215    ) -> Result<Expression>;
216
217    fn mutation_optimize_exists_to_semi_join(
218        &self,
219        expression: &Expression,
220        ctx: &ExecutionContext,
221        outer_tables: &[String],
222        outer_limit: Option<i64>,
223    ) -> Result<Option<Expression>>;
224
225    fn mutation_optimize_in_to_semi_join(
226        &self,
227        expression: &Expression,
228        ctx: &ExecutionContext,
229        outer_tables: &[String],
230    ) -> Result<Option<Expression>>;
231
232    fn mutation_has_subqueries(expression: &Expression) -> bool;
233
234    fn mutation_has_correlated_subqueries(expression: &Expression) -> bool;
235
236    fn mutation_materialize_result(result: Box<dyn QueryResult>) -> Result<RowVec>;
237
238    fn mutation_invalidate_query_cache(&self, table_name: &str);
239
240    fn mutation_invalidate_semantic_cache(&self, table_name: &str);
241
242    fn mutation_invalidate_authorization_caches(&self);
243
244    fn mutation_active_transaction_id(&self) -> Option<i64> {
245        self.mutation_active_transaction()
246            .lock()
247            .unwrap()
248            .as_ref()
249            .map(|state| state.transaction.id())
250    }
251
252    fn mutation_has_active_transaction(&self) -> bool {
253        self.mutation_active_transaction().lock().unwrap().is_some()
254    }
255
256    /// Validate one DDL statement against the transaction-pinned catalog.
257    /// Explicit transactions retain the private generation until COMMIT;
258    /// auto-commit callers receive the exact mutation to attach to their
259    /// storage transaction.
260    fn mutation_stage_catalog_statement(
261        &self,
262        statement: Statement,
263    ) -> Result<Option<CatalogMutationSet>> {
264        let mut active = self.mutation_active_transaction().lock().unwrap();
265        if let Some(state) = active.as_mut() {
266            state.stage_catalog_statement(statement)?;
267            return Ok(None);
268        }
269        drop(active);
270
271        let generation = self.mutation_engine().pin_catalog()?;
272        let mut catalog = DdlTransaction::begin_shared_with_plugin_registry(
273            generation,
274            Arc::clone(self.mutation_plugin_registry()),
275        );
276        catalog.stage_statement(statement)?;
277        catalog.pending_mutation()
278    }
279
280    fn mutation_stage_catalog_statement_as(
281        &self,
282        statement: Statement,
283        actor: radixdb_catalog::ObjectId,
284        current_database: Option<&str>,
285    ) -> Result<Option<CatalogMutationSet>> {
286        let mut active = self.mutation_active_transaction().lock().unwrap();
287        if let Some(state) = active.as_mut() {
288            state
289                .catalog
290                .stage_statement_as(statement, actor, current_database)?;
291            return Ok(None);
292        }
293        drop(active);
294
295        let generation = self.mutation_engine().pin_catalog()?;
296        let mut catalog = DdlTransaction::begin_shared_with_plugin_registry(
297            generation,
298            Arc::clone(self.mutation_plugin_registry()),
299        );
300        catalog.stage_statement_as(statement, actor, current_database)?;
301        catalog.pending_mutation()
302    }
303}