mod binding;
mod binding_types;
mod cache;
mod call;
mod error;
mod external;
pub(crate) mod function;
mod function_binding;
mod host;
mod job;
mod native_function;
mod trigger;
mod value;
#[cfg(test)]
mod call_binding_tests;
use std::collections::BTreeSet;
use std::sync::Arc;
use radixdb_catalog::{
CatalogGeneration, CatalogPayload, ObjectId, ObjectKind, ResourcePolicy, SecurityMode,
Volatility,
};
use radixdb_procedural::{CompileIdentity, VerifiedProgram};
use radixdb_sql::{parse_sql, CreateRoutineStatement, Statement};
pub(crate) use cache::ProceduralProgramCache;
pub(crate) use call::CallBoundary;
pub use call::{ProceduralCallOutcome, ProceduralResultStage};
pub(crate) use job::bind_job_definition;
pub use job::{
JobAttemptMetadata, JobAttemptOutcome, ScheduledJobDefinition, ScheduledJobSchedule,
};
pub(crate) use native_function::catalog_type_name;
pub use radixdb_procedural::{Diagnostic, DiagnosticKind};
pub(crate) use trigger::{
fire_after_row_triggers, fire_before_row_triggers, fire_statement_triggers,
prepare_dml_triggers, validate_trigger_attachment, DmlTriggerEvent, DmlTriggerPlan,
};
use crate::catalog::{
validate_routine_source_contract, PROCEDURAL_COMPILER_ABI, PROCEDURAL_RUNTIME_ABI,
};
#[derive(Debug, Clone)]
pub(super) struct PublishedRoutine {
pub program: VerifiedProgram,
pub owner: ObjectId,
pub security: SecurityMode,
pub volatility: Volatility,
pub resource_policy: ResourcePolicy,
}
pub(crate) fn compile_catalog_routine(
executor: &crate::Executor,
statement: &radixdb_sql::CreateRoutineStatement,
catalog: &radixdb_catalog::CatalogGeneration,
identity: radixdb_procedural::CompileIdentity,
search_path: Vec<radixdb_catalog::ObjectId>,
) -> radixdb_core::Result<Vec<radixdb_catalog::ObjectId>> {
if matches!(
statement.returns,
Some(radixdb_sql::RoutineReturnSyntax::Trigger)
) {
if statement.kind != radixdb_sql::RoutineKindSyntax::Function
|| !statement.arguments.is_empty()
|| !matches!(
statement.volatility,
None | Some(radixdb_sql::RoutineVolatilitySyntax::Volatile)
)
{
return Err(radixdb_core::Error::InvalidArgument(
"RETURNS TRIGGER requires a zero-argument VOLATILE Function".to_string(),
));
}
return Ok(Vec::new());
}
let (program, dependencies) =
compile_verified_catalog_routine(executor, statement, catalog, identity, search_path)?;
drop(program);
Ok(dependencies)
}
fn compile_verified_catalog_routine(
executor: &crate::Executor,
statement: &CreateRoutineStatement,
catalog: &CatalogGeneration,
identity: CompileIdentity,
search_path: Vec<ObjectId>,
) -> radixdb_core::Result<(VerifiedProgram, Vec<ObjectId>)> {
let volatility = match statement.kind {
radixdb_sql::RoutineKindSyntax::Procedure => None,
radixdb_sql::RoutineKindSyntax::Function => Some(match statement.volatility {
Some(radixdb_sql::RoutineVolatilitySyntax::Immutable) => Volatility::Immutable,
Some(radixdb_sql::RoutineVolatilitySyntax::Stable) => Volatility::Stable,
None | Some(radixdb_sql::RoutineVolatilitySyntax::Volatile) => Volatility::Volatile,
}),
};
let mut resolver = binding::ExecutorSemanticResolver::with_search_path(
executor,
catalog,
search_path,
volatility,
);
let compiled = radixdb_procedural::compile_routine(statement, identity, &mut resolver)
.map_err(procedural_compile_error)?;
let program = radixdb_procedural::verify(compiled.program).map_err(procedural_compile_error)?;
Ok((program, compiled.dependencies))
}
pub(super) fn load_published_routine(
executor: &crate::Executor,
routine_id: ObjectId,
expected_kind: ObjectKind,
) -> radixdb_core::Result<Arc<PublishedRoutine>> {
let (catalog, cacheable) = transaction_visible_catalog(executor)?;
let object = catalog.object(routine_id).cloned().ok_or_else(|| {
radixdb_core::Error::InvalidArgument(format!(
"routine object {routine_id} does not exist in the transaction-visible catalog"
))
})?;
if object.kind() != expected_kind {
return Err(radixdb_core::Error::InvalidArgument(format!(
"catalog object {routine_id} is {:?}, expected {expected_kind:?}",
object.kind()
)));
}
let definition = match object.payload() {
CatalogPayload::Function(payload) => payload.procedural_definition().ok_or_else(|| {
radixdb_core::Error::InvalidArgument(format!(
"native function {routine_id} cannot be loaded as a procedural routine"
))
})?,
CatalogPayload::Procedure(payload) => payload.definition(),
_ => {
return Err(radixdb_core::Error::InvalidArgument(format!(
"catalog object {routine_id} has no executable routine definition"
)))
}
};
if definition.compiler_abi() != PROCEDURAL_COMPILER_ABI
|| definition.runtime_abi() != PROCEDURAL_RUNTIME_ABI
{
return Err(radixdb_core::Error::InvalidArgument(format!(
"routine {routine_id} requires unsupported compiler/runtime ABI {}/{}",
definition.compiler_abi(),
definition.runtime_abi()
)));
}
let cache_key = routine_cache_key(catalog.as_ref(), &object)?;
if cacheable {
if let Some(cached) = executor.procedural_cache.get(&cache_key) {
radixdb_procedural::verify(cached.program.program().clone())
.map_err(procedural_compile_error)?;
return Ok(cached);
}
}
let mut statements = parse_sql(definition.source().as_str())
.map_err(|error| radixdb_core::Error::Parse(error.to_string()))?;
if statements.len() != 1 {
return Err(radixdb_core::Error::InvalidArgument(format!(
"routine {routine_id} durable source must contain exactly one CREATE statement"
)));
}
let Statement::CreateRoutine(statement) = statements.remove(0) else {
return Err(radixdb_core::Error::InvalidArgument(format!(
"routine {routine_id} durable source is not CREATE FUNCTION/PROCEDURE"
)));
};
validate_routine_source_contract(&statement, &object, catalog.as_ref())?;
let identity = CompileIdentity {
object_id: object.id(),
definition_revision: object.definition_revision(),
display_name: statement.name.to_string(),
};
let (program, dependencies) = compile_verified_catalog_routine(
executor,
&statement,
catalog.as_ref(),
identity,
definition.search_path().to_vec(),
)?;
let search_path = definition
.search_path()
.iter()
.copied()
.collect::<BTreeSet<_>>();
let compiled_dependencies = dependencies
.into_iter()
.filter(|id| !search_path.contains(id))
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
if compiled_dependencies != definition.dependency_ids() {
return Err(radixdb_core::Error::InvalidArgument(format!(
"routine {routine_id} durable dependency contract is stale or corrupt"
)));
}
let routine = Arc::new(PublishedRoutine {
program,
owner: object.owner_principal_id(),
security: definition.security(),
volatility: definition.volatility(),
resource_policy: definition.resource_policy(),
});
if cacheable {
Ok(executor.procedural_cache.insert(cache_key, routine))
} else {
Ok(routine)
}
}
pub(crate) fn transaction_visible_catalog(
executor: &crate::Executor,
) -> radixdb_core::Result<(Arc<CatalogGeneration>, bool)> {
let active = executor.active_transaction.lock().unwrap();
if let Some(state) = active.as_ref() {
return Ok((
state.catalog.working_generation_shared(),
!state.catalog.has_pending_catalog_changes(),
));
}
drop(active);
executor.engine.pin_catalog().map(|catalog| (catalog, true))
}
fn routine_cache_key(
catalog: &CatalogGeneration,
object: &radixdb_catalog::CatalogObject,
) -> radixdb_core::Result<cache::RoutineCacheKey> {
let definition = match object.payload() {
CatalogPayload::Function(payload) => payload.procedural_definition().ok_or_else(|| {
radixdb_core::Error::InvalidArgument(format!(
"native function {} has no procedural cache key",
object.id()
))
})?,
CatalogPayload::Procedure(payload) => payload.definition(),
_ => {
return Err(radixdb_core::Error::InvalidArgument(format!(
"catalog object {} has no executable routine definition",
object.id()
)))
}
};
let mut dependency_ids = definition
.search_path()
.iter()
.chain(definition.dependency_ids())
.copied()
.collect::<BTreeSet<_>>();
dependency_ids.insert(object.id());
let dependency_versions = dependency_ids
.into_iter()
.map(|id| {
catalog
.object(id)
.map(|dependency| (id, dependency.definition_revision()))
.ok_or_else(|| {
radixdb_core::Error::InvalidArgument(format!(
"routine {} dependency {id} is missing",
object.id()
))
})
})
.collect::<radixdb_core::Result<Vec<_>>>()?;
let meta = catalog.meta();
Ok(cache::RoutineCacheKey {
database_id: meta.database_id(),
catalog_id: meta.catalog_id(),
catalog_generation: meta.catalog_generation(),
object_id: object.id(),
definition_revision: object.definition_revision(),
source_digest: *definition.source().digest(),
dependency_versions,
compiler_abi: definition.compiler_abi(),
runtime_abi: definition.runtime_abi(),
})
}
fn procedural_compile_error(diagnostic: radixdb_procedural::Diagnostic) -> radixdb_core::Error {
radixdb_core::Error::InvalidArgument(format!("procedural compilation failed: {diagnostic}"))
}
#[cfg(test)]
mod tests;