radixdb_executor/procedural/
mod.rs1mod binding;
7mod binding_types;
8mod cache;
9mod call;
10mod error;
11mod external;
12pub(crate) mod function;
13mod function_binding;
14mod host;
15mod job;
16mod native_function;
17mod trigger;
18mod value;
19
20#[cfg(test)]
21mod call_binding_tests;
22
23use std::collections::BTreeSet;
24use std::sync::Arc;
25
26use radixdb_catalog::{
27 CatalogGeneration, CatalogPayload, ObjectId, ObjectKind, ResourcePolicy, SecurityMode,
28 Volatility,
29};
30use radixdb_procedural::{CompileIdentity, VerifiedProgram};
31use radixdb_sql::{parse_sql, CreateRoutineStatement, Statement};
32
33pub(crate) use cache::ProceduralProgramCache;
34pub(crate) use call::CallBoundary;
35pub use call::{ProceduralCallOutcome, ProceduralResultStage};
36pub(crate) use job::bind_job_definition;
37pub use job::{
38 JobAttemptMetadata, JobAttemptOutcome, ScheduledJobDefinition, ScheduledJobSchedule,
39};
40pub(crate) use native_function::catalog_type_name;
41pub use radixdb_procedural::{Diagnostic, DiagnosticKind};
42pub(crate) use trigger::{
43 fire_after_row_triggers, fire_before_row_triggers, fire_statement_triggers,
44 prepare_dml_triggers, validate_trigger_attachment, DmlTriggerEvent, DmlTriggerPlan,
45};
46
47use crate::catalog::{
48 validate_routine_source_contract, PROCEDURAL_COMPILER_ABI, PROCEDURAL_RUNTIME_ABI,
49};
50
51#[derive(Debug, Clone)]
52pub(super) struct PublishedRoutine {
53 pub program: VerifiedProgram,
54 pub owner: ObjectId,
55 pub security: SecurityMode,
56 pub volatility: Volatility,
57 pub resource_policy: ResourcePolicy,
58}
59
60pub(crate) fn compile_catalog_routine(
61 executor: &crate::Executor,
62 statement: &radixdb_sql::CreateRoutineStatement,
63 catalog: &radixdb_catalog::CatalogGeneration,
64 identity: radixdb_procedural::CompileIdentity,
65 search_path: Vec<radixdb_catalog::ObjectId>,
66) -> radixdb_core::Result<Vec<radixdb_catalog::ObjectId>> {
67 if matches!(
68 statement.returns,
69 Some(radixdb_sql::RoutineReturnSyntax::Trigger)
70 ) {
71 if statement.kind != radixdb_sql::RoutineKindSyntax::Function
72 || !statement.arguments.is_empty()
73 || !matches!(
74 statement.volatility,
75 None | Some(radixdb_sql::RoutineVolatilitySyntax::Volatile)
76 )
77 {
78 return Err(radixdb_core::Error::InvalidArgument(
79 "RETURNS TRIGGER requires a zero-argument VOLATILE Function".to_string(),
80 ));
81 }
82 return Ok(Vec::new());
85 }
86 let (program, dependencies) =
87 compile_verified_catalog_routine(executor, statement, catalog, identity, search_path)?;
88 drop(program);
89 Ok(dependencies)
90}
91
92fn compile_verified_catalog_routine(
93 executor: &crate::Executor,
94 statement: &CreateRoutineStatement,
95 catalog: &CatalogGeneration,
96 identity: CompileIdentity,
97 search_path: Vec<ObjectId>,
98) -> radixdb_core::Result<(VerifiedProgram, Vec<ObjectId>)> {
99 let volatility = match statement.kind {
100 radixdb_sql::RoutineKindSyntax::Procedure => None,
101 radixdb_sql::RoutineKindSyntax::Function => Some(match statement.volatility {
102 Some(radixdb_sql::RoutineVolatilitySyntax::Immutable) => Volatility::Immutable,
103 Some(radixdb_sql::RoutineVolatilitySyntax::Stable) => Volatility::Stable,
104 None | Some(radixdb_sql::RoutineVolatilitySyntax::Volatile) => Volatility::Volatile,
105 }),
106 };
107 let mut resolver = binding::ExecutorSemanticResolver::with_search_path(
108 executor,
109 catalog,
110 search_path,
111 volatility,
112 );
113 let compiled = radixdb_procedural::compile_routine(statement, identity, &mut resolver)
114 .map_err(procedural_compile_error)?;
115 let program = radixdb_procedural::verify(compiled.program).map_err(procedural_compile_error)?;
116 Ok((program, compiled.dependencies))
117}
118
119pub(super) fn load_published_routine(
120 executor: &crate::Executor,
121 routine_id: ObjectId,
122 expected_kind: ObjectKind,
123) -> radixdb_core::Result<Arc<PublishedRoutine>> {
124 let (catalog, cacheable) = transaction_visible_catalog(executor)?;
125 let object = catalog.object(routine_id).cloned().ok_or_else(|| {
126 radixdb_core::Error::InvalidArgument(format!(
127 "routine object {routine_id} does not exist in the transaction-visible catalog"
128 ))
129 })?;
130 if object.kind() != expected_kind {
131 return Err(radixdb_core::Error::InvalidArgument(format!(
132 "catalog object {routine_id} is {:?}, expected {expected_kind:?}",
133 object.kind()
134 )));
135 }
136 let definition = match object.payload() {
137 CatalogPayload::Function(payload) => payload.procedural_definition().ok_or_else(|| {
138 radixdb_core::Error::InvalidArgument(format!(
139 "native function {routine_id} cannot be loaded as a procedural routine"
140 ))
141 })?,
142 CatalogPayload::Procedure(payload) => payload.definition(),
143 _ => {
144 return Err(radixdb_core::Error::InvalidArgument(format!(
145 "catalog object {routine_id} has no executable routine definition"
146 )))
147 }
148 };
149 if definition.compiler_abi() != PROCEDURAL_COMPILER_ABI
150 || definition.runtime_abi() != PROCEDURAL_RUNTIME_ABI
151 {
152 return Err(radixdb_core::Error::InvalidArgument(format!(
153 "routine {routine_id} requires unsupported compiler/runtime ABI {}/{}",
154 definition.compiler_abi(),
155 definition.runtime_abi()
156 )));
157 }
158
159 let cache_key = routine_cache_key(catalog.as_ref(), &object)?;
160 if cacheable {
161 if let Some(cached) = executor.procedural_cache.get(&cache_key) {
162 radixdb_procedural::verify(cached.program.program().clone())
165 .map_err(procedural_compile_error)?;
166 return Ok(cached);
167 }
168 }
169
170 let mut statements = parse_sql(definition.source().as_str())
171 .map_err(|error| radixdb_core::Error::Parse(error.to_string()))?;
172 if statements.len() != 1 {
173 return Err(radixdb_core::Error::InvalidArgument(format!(
174 "routine {routine_id} durable source must contain exactly one CREATE statement"
175 )));
176 }
177 let Statement::CreateRoutine(statement) = statements.remove(0) else {
178 return Err(radixdb_core::Error::InvalidArgument(format!(
179 "routine {routine_id} durable source is not CREATE FUNCTION/PROCEDURE"
180 )));
181 };
182 validate_routine_source_contract(&statement, &object, catalog.as_ref())?;
183
184 let identity = CompileIdentity {
185 object_id: object.id(),
186 definition_revision: object.definition_revision(),
187 display_name: statement.name.to_string(),
188 };
189 let (program, dependencies) = compile_verified_catalog_routine(
190 executor,
191 &statement,
192 catalog.as_ref(),
193 identity,
194 definition.search_path().to_vec(),
195 )?;
196 let search_path = definition
197 .search_path()
198 .iter()
199 .copied()
200 .collect::<BTreeSet<_>>();
201 let compiled_dependencies = dependencies
202 .into_iter()
203 .filter(|id| !search_path.contains(id))
204 .collect::<BTreeSet<_>>()
205 .into_iter()
206 .collect::<Vec<_>>();
207 if compiled_dependencies != definition.dependency_ids() {
208 return Err(radixdb_core::Error::InvalidArgument(format!(
209 "routine {routine_id} durable dependency contract is stale or corrupt"
210 )));
211 }
212
213 let routine = Arc::new(PublishedRoutine {
214 program,
215 owner: object.owner_principal_id(),
216 security: definition.security(),
217 volatility: definition.volatility(),
218 resource_policy: definition.resource_policy(),
219 });
220 if cacheable {
221 Ok(executor.procedural_cache.insert(cache_key, routine))
222 } else {
223 Ok(routine)
224 }
225}
226
227pub(crate) fn transaction_visible_catalog(
228 executor: &crate::Executor,
229) -> radixdb_core::Result<(Arc<CatalogGeneration>, bool)> {
230 let active = executor.active_transaction.lock().unwrap();
231 if let Some(state) = active.as_ref() {
232 return Ok((
233 state.catalog.working_generation_shared(),
234 !state.catalog.has_pending_catalog_changes(),
235 ));
236 }
237 drop(active);
238 executor.engine.pin_catalog().map(|catalog| (catalog, true))
239}
240
241fn routine_cache_key(
242 catalog: &CatalogGeneration,
243 object: &radixdb_catalog::CatalogObject,
244) -> radixdb_core::Result<cache::RoutineCacheKey> {
245 let definition = match object.payload() {
246 CatalogPayload::Function(payload) => payload.procedural_definition().ok_or_else(|| {
247 radixdb_core::Error::InvalidArgument(format!(
248 "native function {} has no procedural cache key",
249 object.id()
250 ))
251 })?,
252 CatalogPayload::Procedure(payload) => payload.definition(),
253 _ => {
254 return Err(radixdb_core::Error::InvalidArgument(format!(
255 "catalog object {} has no executable routine definition",
256 object.id()
257 )))
258 }
259 };
260 let mut dependency_ids = definition
261 .search_path()
262 .iter()
263 .chain(definition.dependency_ids())
264 .copied()
265 .collect::<BTreeSet<_>>();
266 dependency_ids.insert(object.id());
267 let dependency_versions = dependency_ids
268 .into_iter()
269 .map(|id| {
270 catalog
271 .object(id)
272 .map(|dependency| (id, dependency.definition_revision()))
273 .ok_or_else(|| {
274 radixdb_core::Error::InvalidArgument(format!(
275 "routine {} dependency {id} is missing",
276 object.id()
277 ))
278 })
279 })
280 .collect::<radixdb_core::Result<Vec<_>>>()?;
281 let meta = catalog.meta();
282 Ok(cache::RoutineCacheKey {
283 database_id: meta.database_id(),
284 catalog_id: meta.catalog_id(),
285 catalog_generation: meta.catalog_generation(),
286 object_id: object.id(),
287 definition_revision: object.definition_revision(),
288 source_digest: *definition.source().digest(),
289 dependency_versions,
290 compiler_abi: definition.compiler_abi(),
291 runtime_abi: definition.runtime_abi(),
292 })
293}
294
295fn procedural_compile_error(diagnostic: radixdb_procedural::Diagnostic) -> radixdb_core::Error {
296 radixdb_core::Error::InvalidArgument(format!("procedural compilation failed: {diagnostic}"))
297}
298
299#[cfg(test)]
300mod tests;