1use std::sync::{Arc, Mutex, OnceLock};
4
5use crate::catalog::DdlTransaction;
6use radixdb_catalog::ObjectId;
7use radixdb_core::{Error, ParamVec, Result, Value};
8use radixdb_functions::FunctionRegistry;
9use radixdb_plugin_host::{
10 DatabasePluginAdmission, ObjectKind as PluginObjectKind, ObjectRequirement, PackageRequirement,
11 PluginRegistry, RequirementIssue,
12};
13use radixdb_sql::ast::{Program, Statement};
14use radixdb_storage::mvcc::engine::MVCCEngine;
15use radixdb_storage::mvcc::ViewDefinition;
16use radixdb_storage::traits::{Engine, QueryResult, Transaction};
17use rustc_hash::FxHashMap;
18
19use crate::aggregation::AggregationExecutorExt;
20use crate::context::{ExecutionContext, TimeoutGuard};
21use crate::mutation::dml::DmlExecutorExt;
22use crate::mutation::dml_fast_path::DmlFastPathExt;
23use crate::mutation::host::ActiveTransaction;
24use crate::mutation::pk_fast_path::PkFastPathExt;
25use crate::navigation;
26use crate::planner::QueryPlanner;
27use crate::procedural::{prepare_dml_triggers, DmlTriggerEvent};
28use crate::query_cache::{CacheStats, CachedPlanRef, QueryCache};
29use crate::result::{self, ExecutionResult};
30use crate::semantic_cache::{SemanticCache, SemanticCacheStatsSnapshot};
31
32#[cfg(any(test, feature = "test-hooks"))]
33thread_local! {
34 static TEST_EXECUTOR_CONSTRUCTIONS: std::cell::Cell<usize> = const {
35 std::cell::Cell::new(0)
36 };
37}
38
39#[cfg(any(test, feature = "test-hooks"))]
40#[doc(hidden)]
41pub fn test_executor_construction_count() -> usize {
42 TEST_EXECUTOR_CONSTRUCTIONS.with(std::cell::Cell::get)
43}
44
45#[cfg(any(test, feature = "test-hooks"))]
46fn record_test_executor_construction() {
47 TEST_EXECUTOR_CONSTRUCTIONS.with(|count| count.set(count.get() + 1));
48}
49
50static DEFAULT_FUNCTION_REGISTRY: OnceLock<Arc<FunctionRegistry>> = OnceLock::new();
51static DEFAULT_PLUGIN_REGISTRY: OnceLock<Arc<PluginRegistry>> = OnceLock::new();
52
53#[inline]
54fn default_function_registry() -> Arc<FunctionRegistry> {
55 DEFAULT_FUNCTION_REGISTRY
56 .get_or_init(|| Arc::new(FunctionRegistry::new()))
57 .clone()
58}
59
60#[inline]
61fn default_plugin_registry() -> Arc<PluginRegistry> {
62 DEFAULT_PLUGIN_REGISTRY
63 .get_or_init(|| Arc::new(PluginRegistry::empty()))
64 .clone()
65}
66
67fn append_plugin_object_requirement(
68 catalog: &radixdb_catalog::CatalogGeneration,
69 requirements: &mut [PackageRequirement],
70 extension_binding_id: ObjectId,
71 requirement: ObjectRequirement,
72) -> Result<()> {
73 let extension = catalog.object(extension_binding_id).ok_or_else(|| {
74 Error::internal("plugin-backed catalog object references a missing extension binding")
75 })?;
76 let radixdb_catalog::CatalogPayload::Extension(extension) = extension.payload() else {
77 return Err(Error::internal(
78 "plugin-backed catalog object references a non-extension catalog object",
79 ));
80 };
81 let package_id = extension.package_id().into_bytes();
82 let package = requirements
83 .iter_mut()
84 .find(|candidate| candidate.package_id == package_id)
85 .ok_or_else(|| Error::internal("plugin package requirement was not constructed"))?;
86 package.objects.push(requirement);
87 Ok(())
88}
89
90pub struct Executor {
95 pub(crate) engine: Arc<MVCCEngine>,
97 pub(crate) function_registry: Arc<FunctionRegistry>,
99 pub(crate) plugin_registry: Arc<PluginRegistry>,
101 pub(crate) query_cache: QueryCache,
103 pub(crate) semantic_cache: Arc<SemanticCache>,
105 pub(crate) feedback_cache: Arc<crate::optimizer::FeedbackCache>,
107 pub(crate) procedural_cache: Arc<crate::procedural::ProceduralProgramCache>,
109 pub(crate) active_transaction: Arc<Mutex<Option<ActiveTransaction>>>,
111 pub(crate) default_isolation_level: Arc<Mutex<radixdb_core::IsolationLevel>>,
115 pub(crate) ddl_fence_already_held: bool,
119 pub(crate) query_planner: std::sync::OnceLock<QueryPlanner>,
121}
122
123impl Executor {
124 pub fn authenticate_principal(&self, login: &str, password: &str) -> Result<ObjectId> {
128 if self.has_active_transaction() {
129 return Err(Error::invalid_argument(
130 "authentication cannot use a connection with an active transaction",
131 ));
132 }
133 self.require_normal_plugin_admission("principal authentication")?;
134 let catalog = self.engine.pin_catalog()?;
135 crate::catalog::security::authenticate_catalog_principal(catalog.as_ref(), login, password)
136 }
137 pub(crate) fn fork_for_stored_function(&self) -> Self {
138 Self {
139 engine: Arc::clone(&self.engine),
140 function_registry: Arc::clone(&self.function_registry),
141 plugin_registry: Arc::clone(&self.plugin_registry),
142 query_cache: QueryCache::default(),
143 semantic_cache: Arc::clone(&self.semantic_cache),
144 feedback_cache: Arc::clone(&self.feedback_cache),
145 procedural_cache: Arc::clone(&self.procedural_cache),
146 active_transaction: Arc::clone(&self.active_transaction),
147 default_isolation_level: Arc::clone(&self.default_isolation_level),
148 ddl_fence_already_held: self.ddl_fence_already_held,
149 query_planner: std::sync::OnceLock::new(),
150 }
151 }
152
153 pub(crate) fn fork_with_owned_ddl_fence(&self) -> Self {
158 let mut executor = self.fork_for_stored_function();
159 executor.ddl_fence_already_held = true;
160 executor
161 }
162
163 fn install_storage_binders(engine: &MVCCEngine, plugin_registry: Arc<PluginRegistry>) {
164 engine.install_row_validator_binder(crate::mutation::row_validation::bind);
165 engine.install_view_dependency_binder(crate::mutation::view_binding::bind_from_sql);
166 engine.install_catalog_runtime_binder(crate::catalog::plugin_catalog_runtime_binder(
167 plugin_registry,
168 ));
169 }
170
171 pub(crate) fn visible_view_lowercase(
174 &self,
175 name_lower: &str,
176 ) -> Result<Option<Arc<ViewDefinition>>> {
177 let active = self.active_transaction.lock().unwrap();
178 if let Some(state) = active.as_ref() {
179 return crate::catalog::bind_runtime_view(
180 state.catalog.working_generation(),
181 name_lower,
182 )
183 .map(|view| view.map(Arc::new));
184 }
185 drop(active);
186 self.engine.get_view_lowercase(name_lower)
187 }
188
189 pub(crate) fn visible_view(&self, name: &str) -> Result<Option<Arc<ViewDefinition>>> {
190 self.visible_view_lowercase(&name.to_lowercase())
191 }
192
193 pub(crate) fn visible_view_names(&self) -> Result<Vec<String>> {
194 let active = self.active_transaction.lock().unwrap();
195 if let Some(state) = active.as_ref() {
196 return Ok(crate::catalog::list_runtime_views(
197 state.catalog.working_generation(),
198 ));
199 }
200 drop(active);
201 self.engine.list_views()
202 }
203
204 #[doc(hidden)]
205 pub fn describe_query_output(
206 &self,
207 sql: &str,
208 ) -> Result<Option<Vec<crate::binding::output::QueryOutputColumn>>> {
209 crate::binding::output::OutputBindingExt::describe_query_output(self, sql)
210 }
211
212 pub fn new(engine: Arc<MVCCEngine>) -> Self {
214 #[cfg(any(test, feature = "test-hooks"))]
215 record_test_executor_construction();
216 let plugin_registry = default_plugin_registry();
217 Self::install_storage_binders(&engine, Arc::clone(&plugin_registry));
218 let default_isolation_level = engine.registry().get_global_isolation_level();
219 Self {
220 engine,
221 function_registry: default_function_registry(),
222 plugin_registry,
223 query_cache: QueryCache::default(),
224 semantic_cache: Arc::new(SemanticCache::default()),
225 feedback_cache: Arc::new(crate::optimizer::FeedbackCache::new()),
226 procedural_cache: Arc::default(),
227 active_transaction: Arc::new(Mutex::new(None)),
228 default_isolation_level: Arc::new(Mutex::new(default_isolation_level)),
229 ddl_fence_already_held: false,
230 query_planner: std::sync::OnceLock::new(),
231 }
232 }
233
234 pub fn with_function_registry(
236 engine: Arc<MVCCEngine>,
237 function_registry: Arc<FunctionRegistry>,
238 ) -> Self {
239 #[cfg(any(test, feature = "test-hooks"))]
240 record_test_executor_construction();
241 let plugin_registry = default_plugin_registry();
242 Self::install_storage_binders(&engine, Arc::clone(&plugin_registry));
243 let default_isolation_level = engine.registry().get_global_isolation_level();
244 Self {
245 engine,
246 function_registry,
247 plugin_registry,
248 query_cache: QueryCache::default(),
249 semantic_cache: Arc::new(SemanticCache::default()),
250 feedback_cache: Arc::new(crate::optimizer::FeedbackCache::new()),
251 procedural_cache: Arc::default(),
252 active_transaction: Arc::new(Mutex::new(None)),
253 default_isolation_level: Arc::new(Mutex::new(default_isolation_level)),
254 ddl_fence_already_held: false,
255 query_planner: std::sync::OnceLock::new(),
256 }
257 }
258
259 pub fn with_cache_size(engine: Arc<MVCCEngine>, cache_size: usize) -> Self {
261 #[cfg(any(test, feature = "test-hooks"))]
262 record_test_executor_construction();
263 let plugin_registry = default_plugin_registry();
264 Self::install_storage_binders(&engine, Arc::clone(&plugin_registry));
265 let default_isolation_level = engine.registry().get_global_isolation_level();
266 Self {
267 engine,
268 function_registry: default_function_registry(),
269 plugin_registry,
270 query_cache: QueryCache::new(cache_size),
271 semantic_cache: Arc::new(SemanticCache::default()),
272 feedback_cache: Arc::new(crate::optimizer::FeedbackCache::new()),
273 procedural_cache: Arc::default(),
274 active_transaction: Arc::new(Mutex::new(None)),
275 default_isolation_level: Arc::new(Mutex::new(default_isolation_level)),
276 ddl_fence_already_held: false,
277 query_planner: std::sync::OnceLock::new(),
278 }
279 }
280
281 #[doc(hidden)]
284 pub fn with_shared_runtime_caches(
285 engine: Arc<MVCCEngine>,
286 semantic_cache: Arc<SemanticCache>,
287 feedback_cache: Arc<crate::optimizer::FeedbackCache>,
288 ) -> Self {
289 #[cfg(any(test, feature = "test-hooks"))]
290 record_test_executor_construction();
291 let plugin_registry = default_plugin_registry();
292 Self::install_storage_binders(&engine, Arc::clone(&plugin_registry));
293 let default_isolation_level = engine.registry().get_global_isolation_level();
294 Self {
295 engine,
296 function_registry: default_function_registry(),
297 plugin_registry,
298 query_cache: QueryCache::default(),
299 semantic_cache,
300 feedback_cache,
301 procedural_cache: Arc::default(),
302 active_transaction: Arc::new(Mutex::new(None)),
303 default_isolation_level: Arc::new(Mutex::new(default_isolation_level)),
304 ddl_fence_already_held: false,
305 query_planner: std::sync::OnceLock::new(),
306 }
307 }
308
309 #[doc(hidden)]
312 pub fn with_shared_runtime_caches_and_plugin_registry(
313 engine: Arc<MVCCEngine>,
314 semantic_cache: Arc<SemanticCache>,
315 feedback_cache: Arc<crate::optimizer::FeedbackCache>,
316 plugin_registry: Arc<PluginRegistry>,
317 ) -> Self {
318 #[cfg(any(test, feature = "test-hooks"))]
319 record_test_executor_construction();
320 Self::install_storage_binders(&engine, Arc::clone(&plugin_registry));
321 let default_isolation_level = engine.registry().get_global_isolation_level();
322 Self {
323 engine,
324 function_registry: default_function_registry(),
325 plugin_registry,
326 query_cache: QueryCache::default(),
327 semantic_cache,
328 feedback_cache,
329 procedural_cache: Arc::default(),
330 active_transaction: Arc::new(Mutex::new(None)),
331 default_isolation_level: Arc::new(Mutex::new(default_isolation_level)),
332 ddl_fence_already_held: false,
333 query_planner: std::sync::OnceLock::new(),
334 }
335 }
336
337 #[doc(hidden)]
339 pub fn with_plugin_registry(
340 engine: Arc<MVCCEngine>,
341 plugin_registry: Arc<PluginRegistry>,
342 ) -> Self {
343 #[cfg(any(test, feature = "test-hooks"))]
344 record_test_executor_construction();
345 Self::install_storage_binders(&engine, Arc::clone(&plugin_registry));
346 let default_isolation_level = engine.registry().get_global_isolation_level();
347 Self {
348 engine,
349 function_registry: default_function_registry(),
350 plugin_registry,
351 query_cache: QueryCache::default(),
352 semantic_cache: Arc::new(SemanticCache::default()),
353 feedback_cache: Arc::new(crate::optimizer::FeedbackCache::new()),
354 procedural_cache: Arc::default(),
355 active_transaction: Arc::new(Mutex::new(None)),
356 default_isolation_level: Arc::new(Mutex::new(default_isolation_level)),
357 ddl_fence_already_held: false,
358 query_planner: std::sync::OnceLock::new(),
359 }
360 }
361
362 #[doc(hidden)]
365 pub fn plugin_admission(&self) -> Result<DatabasePluginAdmission> {
366 let catalog = self.engine.pin_catalog()?;
367 let mut requirements = catalog
368 .objects_of_kind(radixdb_catalog::ObjectKind::Extension)
369 .map(|object| {
370 let radixdb_catalog::CatalogPayload::Extension(payload) = object.payload() else {
371 return Err(Error::internal(
372 "catalog admitted Extension with a different payload",
373 ));
374 };
375 PackageRequirement::for_package_binding(
376 payload.package_id().into_bytes(),
377 payload.version(),
378 payload.abi_major(),
379 payload.abi_min_minor(),
380 payload.abi_max_minor(),
381 *payload.descriptor_fingerprint(),
382 )
383 .map_err(|detail| {
384 Error::internal(format!(
385 "catalog admitted an invalid extension requirement: {detail}"
386 ))
387 })
388 })
389 .collect::<Result<Vec<_>>>()?;
390 for object in catalog.objects_of_kind(radixdb_catalog::ObjectKind::ExternalType) {
391 let radixdb_catalog::CatalogPayload::ExternalType(payload) = object.payload() else {
392 return Err(Error::internal(
393 "catalog admitted ExternalType with a different payload",
394 ));
395 };
396 append_plugin_object_requirement(
397 catalog.as_ref(),
398 &mut requirements,
399 payload.extension_binding_id(),
400 ObjectRequirement {
401 object_id: object.id().into_bytes(),
402 kind: PluginObjectKind::ExternalType,
403 codec_version: Some(payload.write_codec_version()),
404 semantic_revision: Some(payload.semantic_revision()),
405 },
406 )?;
407 }
408 for object in catalog.objects_of_kind(radixdb_catalog::ObjectKind::Function) {
409 let radixdb_catalog::CatalogPayload::Function(payload) = object.payload() else {
410 return Err(Error::internal(
411 "catalog admitted Function with a different payload",
412 ));
413 };
414 let Some(native) = payload.native_definition() else {
415 continue;
416 };
417 append_plugin_object_requirement(
418 catalog.as_ref(),
419 &mut requirements,
420 native.extension_binding_id(),
421 ObjectRequirement {
422 object_id: object.id().into_bytes(),
423 kind: PluginObjectKind::Function,
424 codec_version: None,
425 semantic_revision: Some(native.semantic_revision()),
426 },
427 )?;
428 }
429 for object in catalog.objects_of_kind(radixdb_catalog::ObjectKind::Operator) {
430 let radixdb_catalog::CatalogPayload::Operator(payload) = object.payload() else {
431 return Err(Error::internal(
432 "catalog admitted Operator with a different payload",
433 ));
434 };
435 append_plugin_object_requirement(
436 catalog.as_ref(),
437 &mut requirements,
438 payload.extension_binding_id(),
439 ObjectRequirement {
440 object_id: object.id().into_bytes(),
441 kind: PluginObjectKind::Operator,
442 codec_version: None,
443 semantic_revision: Some(payload.semantic_revision()),
444 },
445 )?;
446 }
447 for object in catalog.objects_of_kind(radixdb_catalog::ObjectKind::OperatorClass) {
448 let radixdb_catalog::CatalogPayload::OperatorClass(payload) = object.payload() else {
449 return Err(Error::internal(
450 "catalog admitted OperatorClass with a different payload",
451 ));
452 };
453 append_plugin_object_requirement(
454 catalog.as_ref(),
455 &mut requirements,
456 payload.extension_binding_id(),
457 ObjectRequirement {
458 object_id: object.id().into_bytes(),
459 kind: PluginObjectKind::OperatorClass,
460 codec_version: Some(payload.key_codec_revision()),
461 semantic_revision: Some(payload.semantic_revision()),
462 },
463 )?;
464 }
465 for object in catalog.objects_of_kind(radixdb_catalog::ObjectKind::PlannerSupport) {
466 let radixdb_catalog::CatalogPayload::PlannerSupport(payload) = object.payload() else {
467 return Err(Error::internal(
468 "catalog admitted PlannerSupport with a different payload",
469 ));
470 };
471 append_plugin_object_requirement(
472 catalog.as_ref(),
473 &mut requirements,
474 payload.extension_binding_id(),
475 ObjectRequirement {
476 object_id: object.id().into_bytes(),
477 kind: PluginObjectKind::PlannerSupport,
478 codec_version: None,
479 semantic_revision: Some(payload.semantic_revision()),
480 },
481 )?;
482 }
483 Ok(self.plugin_registry.assess_requirements(&requirements))
484 }
485
486 #[doc(hidden)]
487 pub fn plugin_admission_diagnostic(&self) -> Result<Option<String>> {
488 Ok(match self.plugin_admission()? {
489 DatabasePluginAdmission::Normal => None,
490 DatabasePluginAdmission::Restricted { issues } => {
491 Some(restricted_plugin_error("ordinary access", &issues).to_string())
492 }
493 })
494 }
495
496 fn require_normal_plugin_admission(&self, operation: &str) -> Result<()> {
497 match self.plugin_admission()? {
498 DatabasePluginAdmission::Normal => Ok(()),
499 DatabasePluginAdmission::Restricted { issues } => {
500 Err(restricted_plugin_error(operation, &issues))
501 }
502 }
503 }
504
505 fn admit_statement_for_plugins(
506 &self,
507 statement: &Statement,
508 context: &ExecutionContext,
509 ) -> Result<()> {
510 match self.plugin_admission()? {
511 DatabasePluginAdmission::Normal => Ok(()),
512 DatabasePluginAdmission::Restricted { .. }
513 if matches!(statement, Statement::DropExtension(_))
514 && context.effective_principal_id() == ObjectId::BOOTSTRAP_OWNER =>
515 {
516 Ok(())
517 }
518 DatabasePluginAdmission::Restricted { issues } => {
519 Err(restricted_plugin_error("SQL execution", &issues))
520 }
521 }
522 }
523
524 #[doc(hidden)]
529 pub fn new_with_owned_ddl_fence(
530 engine: Arc<MVCCEngine>,
531 plugin_registry: Arc<PluginRegistry>,
532 ) -> Self {
533 let mut executor = Self::with_plugin_registry(engine, plugin_registry);
534 executor.ddl_fence_already_held = true;
535 executor
536 }
537
538 #[doc(hidden)]
539 pub fn plugin_registry(&self) -> Arc<PluginRegistry> {
540 Arc::clone(&self.plugin_registry)
541 }
542
543 #[doc(hidden)]
546 pub fn visit_logical_export_rows(
547 &self,
548 table_name: &str,
549 visitor: &mut dyn FnMut(i64, radixdb_core::Row) -> Result<()>,
550 ) -> Result<()> {
551 if !self.ddl_fence_already_held {
552 return Err(Error::internal(
553 "logical export row scan requires an owned catalog fence",
554 ));
555 }
556 let active = self.active_transaction.lock().unwrap();
557 let state = active.as_ref().ok_or(Error::TransactionNotStarted)?;
558 state
559 .transaction
560 .get_table(table_name)?
561 .visit_visible_rows(visitor)
562 }
563
564 pub fn has_active_transaction(&self) -> bool {
566 self.active_transaction.lock().unwrap().is_some()
567 }
568
569 #[doc(hidden)]
571 pub fn active_transaction_id(&self) -> Option<i64> {
572 self.active_transaction
573 .lock()
574 .unwrap()
575 .as_ref()
576 .map(|state| state.transaction.id())
577 }
578
579 #[doc(hidden)]
580 pub fn create_active_savepoint(&self, name: &str) -> Result<()> {
581 let mut active = self.active_transaction.lock().unwrap();
582 active
583 .as_mut()
584 .ok_or(Error::TransactionNotStarted)?
585 .create_savepoint(name)
586 }
587
588 #[doc(hidden)]
589 pub fn rollback_active_to_savepoint(&self, name: &str) -> Result<()> {
590 let mut active = self.active_transaction.lock().unwrap();
591 active
592 .as_mut()
593 .ok_or(Error::TransactionNotStarted)?
594 .rollback_to_savepoint(name)
595 }
596
597 #[doc(hidden)]
598 pub fn release_active_savepoint(&self, name: &str) -> Result<()> {
599 let mut active = self.active_transaction.lock().unwrap();
600 active
601 .as_mut()
602 .ok_or(Error::TransactionNotStarted)?
603 .release_savepoint(name)
604 }
605
606 #[doc(hidden)]
609 pub fn commit_installed_transaction(&self) -> Result<()> {
610 let mut active = self.active_transaction.lock().unwrap();
611 let mut state = active.take().ok_or(Error::TransactionNotStarted)?;
612 let _catalog_write_fence = state
613 .has_pending_catalog_changes()
614 .then(|| self.engine.acquire_catalog_write_fence());
615 if let Err(error) = state.stage_catalog_for_commit() {
616 *active = Some(state);
617 return Err(error);
618 }
619 match state.transaction.commit() {
620 Ok(()) => Ok(()),
621 Err(error) => {
622 if state.transaction.is_active() {
623 *active = Some(state);
624 }
625 Err(error)
626 }
627 }
628 }
629
630 #[doc(hidden)]
631 pub fn rollback_installed_transaction(&self) -> Result<()> {
632 let mut active = self.active_transaction.lock().unwrap();
633 let mut state = active.take().ok_or(Error::TransactionNotStarted)?;
634 state.rollback()
635 }
636
637 pub(crate) fn get_query_planner(&self) -> &QueryPlanner {
639 self.query_planner.get_or_init(|| {
640 QueryPlanner::with_feedback_cache(
641 Arc::clone(&self.engine),
642 Arc::clone(&self.feedback_cache),
643 )
644 })
645 }
646
647 pub(crate) fn bind_cached_reference_expand(
648 &self,
649 statement: &Statement,
650 cached: &Arc<std::sync::RwLock<navigation::CachedReferenceExpand>>,
651 ) -> Result<Option<navigation::ReferenceExpandPlan>> {
652 let select = match statement {
653 Statement::Select(select) => select,
654 Statement::Explain(explain) => match explain.statement.as_ref() {
655 Statement::Select(select) => select,
656 _ => {
657 navigation::reject_navigation_in_write_statement(
658 self.engine.as_ref(),
659 statement,
660 )?;
661 return Ok(None);
662 }
663 },
664 _ => {
665 navigation::reject_navigation_in_write_statement(self.engine.as_ref(), statement)?;
666 return Ok(None);
667 }
668 };
669 let schema_scope_id = self.engine.schema_scope_id();
670 let schema_generation = self.engine.schema_epoch();
671
672 {
673 let binding = cached
674 .read()
675 .map_err(|_| Error::LockAcquisitionFailed("reference expand cache".to_string()))?;
676 match &*binding {
677 navigation::CachedReferenceExpand::NoPaths {
678 schema_scope_id: cached_scope,
679 schema_generation: cached_generation,
680 } if *cached_scope == schema_scope_id
681 && *cached_generation == schema_generation =>
682 {
683 return Ok(None);
684 }
685 navigation::CachedReferenceExpand::Plan(plan)
686 if plan.schema_scope_id() == schema_scope_id
687 && plan.schema_generation() == schema_generation =>
688 {
689 return Ok(Some(plan.clone()));
690 }
691 _ => {}
692 }
693 }
694
695 let plan = navigation::bind_reference_expand_plan(self.engine.as_ref(), select)?;
696 let mut binding = cached
697 .write()
698 .map_err(|_| Error::LockAcquisitionFailed("reference expand cache".to_string()))?;
699 *binding = match &plan {
700 Some(plan) => navigation::CachedReferenceExpand::Plan(plan.clone()),
701 None => navigation::CachedReferenceExpand::NoPaths {
702 schema_scope_id,
703 schema_generation,
704 },
705 };
706 Ok(plan)
707 }
708
709 pub fn set_default_isolation_level(&self, level: radixdb_core::IsolationLevel) {
711 *self.default_isolation_level.lock().unwrap() = level;
712 }
713
714 pub fn default_isolation_level(&self) -> radixdb_core::IsolationLevel {
716 *self.default_isolation_level.lock().unwrap()
717 }
718
719 pub fn engine(&self) -> &Arc<MVCCEngine> {
721 &self.engine
722 }
723
724 pub fn function_registry(&self) -> &Arc<FunctionRegistry> {
726 &self.function_registry
727 }
728
729 pub fn execute(&self, sql: &str) -> Result<ExecutionResult> {
735 let ctx = ExecutionContext::new();
736 self.execute_with_context(sql, &ctx)
737 }
738
739 pub fn execute_with_params(&self, sql: &str, params: ParamVec) -> Result<ExecutionResult> {
745 if params.is_empty() {
746 return self.execute(sql);
747 }
748 if !params.iter().any(|value| value.as_external().is_some()) {
749 if let Some(result) = self.try_fast_path_with_params(sql, ¶ms) {
750 return result;
751 }
752 }
753 let ctx = ExecutionContext::with_params(params);
754 self.execute_with_context(sql, &ctx)
755 }
756
757 pub fn try_fast_path_with_params(
760 &self,
761 sql: &str,
762 params: &[Value],
763 ) -> Option<Result<ExecutionResult>> {
764 crate::dispatch::program::try_fast_path_with_params(self, sql, params)
765 }
766
767 pub fn execute_with_named_params(
772 &self,
773 sql: &str,
774 params: FxHashMap<String, Value>,
775 ) -> Result<ExecutionResult> {
776 let ctx = ExecutionContext::with_named_params(params);
777 self.execute_with_context(sql, &ctx)
778 }
779
780 pub fn execute_with_context(
783 &self,
784 sql: &str,
785 ctx: &ExecutionContext,
786 ) -> Result<ExecutionResult> {
787 let timeout_guard = TimeoutGuard::new(ctx);
788 let result = self.execute_cached(sql, ctx)?;
789 Ok(result::TimedQueryResult::wrap_with_workload(
790 result,
791 timeout_guard,
792 ctx.cancellation_handle(),
793 sql,
794 ))
795 }
796
797 fn execute_cached(&self, sql: &str, ctx: &ExecutionContext) -> Result<Box<dyn QueryResult>> {
803 crate::dispatch::program::execute_sql(self, sql, ctx)
804 }
805
806 pub fn query_cache(&self) -> &QueryCache {
808 &self.query_cache
809 }
810
811 pub fn cache_stats(&self) -> CacheStats {
813 self.query_cache.stats()
814 }
815
816 pub fn clear_cache(&self) {
818 self.query_cache.clear();
819 self.procedural_cache.clear();
820 }
821
822 pub fn semantic_cache(&self) -> &SemanticCache {
824 &self.semantic_cache
825 }
826
827 pub fn semantic_cache_stats(&self) -> SemanticCacheStatsSnapshot {
829 self.semantic_cache.stats()
830 }
831
832 pub fn clear_semantic_cache(&self) {
834 self.semantic_cache.clear();
835 self.feedback_cache.clear();
836 }
837
838 pub fn invalidate_semantic_cache(&self, table_name: &str) {
842 self.semantic_cache.invalidate_table(table_name);
843 self.feedback_cache.invalidate_table(table_name);
844 }
845
846 pub fn execute_program(&self, program: &Program) -> Result<ExecutionResult> {
848 let ctx = ExecutionContext::new();
849 self.execute_program_with_context(program, &ctx)
850 }
851
852 pub fn execute_program_with_context(
854 &self,
855 program: &Program,
856 ctx: &ExecutionContext,
857 ) -> Result<ExecutionResult> {
858 crate::dispatch::program::execute_program(self, program, ctx)
859 }
860
861 pub fn execute_statement(
863 &self,
864 statement: &Statement,
865 ctx: &ExecutionContext,
866 ) -> Result<ExecutionResult> {
867 self.execute_statement_inner(statement, ctx, false, None)
868 }
869
870 fn execute_statement_after_navigation(
871 &self,
872 statement: &Statement,
873 ctx: &ExecutionContext,
874 ) -> Result<ExecutionResult> {
875 self.execute_statement_inner(statement, ctx, true, None)
876 }
877
878 fn execute_statement_with_navigation_plan(
879 &self,
880 statement: &Statement,
881 ctx: &ExecutionContext,
882 plan: Option<navigation::ReferenceExpandPlan>,
883 ) -> Result<Box<dyn QueryResult>> {
884 self.execute_statement_inner(statement, ctx, true, plan)
885 }
886
887 fn execute_statement_inner(
888 &self,
889 statement: &Statement,
890 ctx: &ExecutionContext,
891 navigation_checked: bool,
892 reference_expand: Option<navigation::ReferenceExpandPlan>,
893 ) -> Result<Box<dyn QueryResult>> {
894 self.admit_statement_for_plugins(statement, ctx)?;
895 for value in ctx.params().iter().chain(ctx.named_params().values()) {
896 if value.as_external().is_some() {
897 self.plugin_registry
898 .validate_external_value(value)
899 .map_err(|error| Error::invalid_argument(error.to_string()))?;
900 }
901 }
902 let bound_context;
903 let ctx = if ctx.stored_function_invoker().is_some() {
904 ctx
905 } else {
906 let invoker: Arc<dyn crate::context::StoredFunctionInvoker> = Arc::new(
907 crate::procedural::function::ExecutorStoredFunctionInvoker::new(self, ctx),
908 );
909 bound_context = ctx.clone().with_stored_function_invoker(invoker);
910 &bound_context
911 };
912
913 let function_boundary = if !self.has_active_transaction()
919 && crate::procedural::function::statement_calls_stored_function(self, statement)?
920 {
921 Some(self.begin_procedural_boundary()?)
922 } else {
923 None
924 };
925 let result = crate::dispatch::statement::execute_statement(
926 self,
927 statement,
928 ctx,
929 navigation_checked,
930 reference_expand,
931 );
932 match (result, function_boundary) {
933 (Ok(result), Some(boundary)) if matches!(statement, Statement::Select(_)) => Ok(
934 crate::procedural::function::wrap_function_statement_result(self, result, boundary),
935 ),
936 (Ok(result), Some(boundary)) => match self.complete_procedural_boundary(&boundary) {
937 Ok(()) => Ok(result),
938 Err(error) => {
939 let _ = self.abort_procedural_boundary(&boundary);
940 Err(error)
941 }
942 },
943 (Err(error), Some(boundary)) => {
944 let _ = self.abort_procedural_boundary(&boundary);
945 Err(error)
946 }
947 (Ok(result), None) => Ok(result),
948 (Err(error), None) => Err(error),
949 }
950 }
951
952 #[doc(hidden)]
958 pub fn install_transaction(&self, tx: Box<dyn Transaction>) {
959 let mut active_tx = self.active_transaction.lock().unwrap();
960 let catalog = self
961 .engine
962 .pin_catalog()
963 .expect("an installed transaction belongs to an open catalog owner");
964 *active_tx = Some(ActiveTransaction::new(
965 tx,
966 DdlTransaction::begin_shared_with_plugin_registry(
967 catalog,
968 Arc::clone(&self.plugin_registry),
969 ),
970 ));
971 }
972
973 pub fn begin_transaction(&self) -> Result<Box<dyn Transaction>> {
975 self.engine
976 .begin_transaction_with_level(self.default_isolation_level())
977 }
978
979 pub fn begin_transaction_with_isolation(
981 &self,
982 isolation: radixdb_core::IsolationLevel,
983 ) -> Result<Box<dyn Transaction>> {
984 self.engine.begin_transaction_with_level(isolation)
985 }
986
987 pub fn get_or_create_plan(&self, sql: &str) -> Result<CachedPlanRef> {
993 crate::dispatch::program::get_or_create_plan(self, sql)
994 }
995
996 pub fn execute_with_cached_plan(
1003 &self,
1004 plan: &CachedPlanRef,
1005 ctx: &ExecutionContext,
1006 ) -> Result<ExecutionResult> {
1007 crate::dispatch::program::execute_prepared_plan(self, plan, ctx)
1008 }
1009
1010 pub(crate) fn execute_bound_cached_plan(
1011 &self,
1012 plan: &CachedPlanRef,
1013 ctx: &ExecutionContext,
1014 ) -> Result<ExecutionResult> {
1015 self.admit_statement_for_plugins(plan.statement.as_ref(), ctx)?;
1016 for value in ctx.params().iter().chain(ctx.named_params().values()) {
1017 if value.as_external().is_some() {
1018 self.plugin_registry
1019 .validate_external_value(value)
1020 .map_err(|error| Error::invalid_argument(error.to_string()))?;
1021 }
1022 }
1023 let bound_context;
1024 let ctx = if ctx.stored_function_invoker().is_some() {
1025 ctx
1026 } else {
1027 let invoker: Arc<dyn crate::context::StoredFunctionInvoker> = Arc::new(
1028 crate::procedural::function::ExecutorStoredFunctionInvoker::new(self, ctx),
1029 );
1030 bound_context = ctx.clone().with_stored_function_invoker(invoker);
1031 &bound_context
1032 };
1033 crate::authorization::authorize_statement(self, plan.statement.as_ref(), ctx)?;
1038 let reference_expand =
1039 self.bind_cached_reference_expand(plan.statement.as_ref(), &plan.reference_expand)?;
1040 if reference_expand.is_some() && matches!(plan.statement.as_ref(), Statement::Select(_)) {
1041 return self.execute_statement_with_navigation_plan(
1042 &plan.statement,
1043 ctx,
1044 reference_expand,
1045 );
1046 }
1047
1048 match plan.statement.as_ref() {
1050 Statement::Select(stmt) => {
1051 if let Some(result) = self.try_fast_pk_lookup_compiled(stmt, ctx, &plan.compiled) {
1052 return result;
1053 }
1054 if let Some(result) = self.try_fast_count_distinct_compiled(stmt, &plan.compiled) {
1055 return result;
1056 }
1057 if let Some(result) = self.try_fast_count_star_compiled(stmt, &plan.compiled) {
1058 return result;
1059 }
1060 }
1061 Statement::Update(stmt) => {
1062 let updated_columns = stmt
1063 .updates
1064 .keys()
1065 .map(ToString::to_string)
1066 .collect::<Vec<_>>();
1067 let triggers = prepare_dml_triggers(
1068 self,
1069 stmt.table_name.value_lower.as_str(),
1070 DmlTriggerEvent::Update,
1071 &updated_columns,
1072 ctx,
1073 )?;
1074 if triggers.is_empty() && self.active_transaction.lock().unwrap().is_none() {
1075 if let Some(result) =
1076 self.try_fast_pk_update_compiled(stmt, ctx, &plan.compiled)
1077 {
1078 return result;
1079 }
1080 }
1081 }
1082 Statement::Delete(stmt) => {
1083 let triggers = prepare_dml_triggers(
1084 self,
1085 stmt.table_name.value_lower.as_str(),
1086 DmlTriggerEvent::Delete,
1087 &[],
1088 ctx,
1089 )?;
1090 if triggers.is_empty() && self.active_transaction.lock().unwrap().is_none() {
1091 if let Some(result) =
1092 self.try_fast_pk_delete_compiled(stmt, ctx, &plan.compiled)
1093 {
1094 return result;
1095 }
1096 }
1097 }
1098 Statement::Insert(stmt) if self.active_transaction.lock().unwrap().is_none() => {
1099 return self.execute_insert_with_compiled_cache(stmt, ctx, &plan.compiled);
1100 }
1101 _ => {}
1102 }
1103
1104 self.execute_statement_after_navigation(&plan.statement, ctx)
1105 }
1106}
1107
1108fn restricted_plugin_error(operation: &str, issues: &[RequirementIssue]) -> Error {
1109 const MAX_REPORTED_ISSUES: usize = 16;
1110 let mut details = issues
1111 .iter()
1112 .take(MAX_REPORTED_ISSUES)
1113 .map(|issue| match issue {
1114 RequirementIssue::MissingPackage { package_id } => {
1115 format!("missing package {}", hex_package_id(package_id))
1116 }
1117 RequirementIssue::PackageVersion {
1118 package_id,
1119 required,
1120 active,
1121 } => format!(
1122 "package {} requires version {required}, active version is {active}",
1123 hex_package_id(package_id)
1124 ),
1125 RequirementIssue::PackageAbi {
1126 package_id,
1127 required_major,
1128 required_min_minor,
1129 required_max_minor,
1130 active_major,
1131 active_min_minor,
1132 active_max_minor,
1133 } => format!(
1134 "package {} requires ABI {required_major}.{required_min_minor}..={required_major}.{required_max_minor}, active package declares ABI {active_major}.{active_min_minor}..={active_major}.{active_max_minor}",
1135 hex_package_id(package_id)
1136 ),
1137 RequirementIssue::DescriptorFingerprint { package_id } => format!(
1138 "package {} descriptor fingerprint differs",
1139 hex_package_id(package_id)
1140 ),
1141 RequirementIssue::MissingOrStaleObject {
1142 package_id,
1143 object_id,
1144 kind,
1145 } => format!(
1146 "package {} has missing/stale {:?} object {}",
1147 hex_package_id(package_id),
1148 kind,
1149 hex_package_id(object_id)
1150 ),
1151 })
1152 .collect::<Vec<_>>();
1153 if issues.len() > MAX_REPORTED_ISSUES {
1154 details.push(format!(
1155 "{} additional dependency issues omitted",
1156 issues.len() - MAX_REPORTED_ISSUES
1157 ));
1158 }
1159 Error::NotSupported(format!(
1160 "database is in restricted plugin diagnostic mode; {operation} is unavailable: {}",
1161 details.join("; ")
1162 ))
1163}
1164
1165fn hex_package_id(id: &[u8; 16]) -> String {
1166 id.iter().map(|byte| format!("{byte:02x}")).collect()
1167}
1168
1169#[doc(hidden)]
1173pub fn count_parameters(stmt: &Statement) -> (bool, usize) {
1174 crate::dispatch::program::count_parameters(stmt)
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179 use super::*;
1180 use radixdb_storage::mvcc::engine::MVCCEngine;
1181
1182 fn create_test_executor() -> Executor {
1183 let engine = MVCCEngine::in_memory();
1184 engine.open_engine().unwrap();
1185 Executor::new(Arc::new(engine))
1186 }
1187
1188 #[test]
1189 fn test_executor_creation() {
1190 let executor = create_test_executor();
1191 assert!(executor.function_registry().exists("COUNT"));
1192 assert!(executor.function_registry().exists("UPPER"));
1193 }
1194
1195 #[test]
1196 fn test_empty_program() {
1197 let executor = create_test_executor();
1198 match executor.execute("") {
1199 Err(error) => assert_eq!(error, Error::NoStatementsToExecute),
1200 Ok(_) => panic!("empty SQL must be rejected"),
1201 }
1202 }
1203
1204 #[test]
1205 fn test_create_table() {
1206 let executor = create_test_executor();
1207 let result = executor
1208 .execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
1209 .unwrap();
1210 assert_eq!(result.rows_affected(), 0);
1211 }
1212
1213 #[test]
1214 fn test_insert_and_select() {
1215 let executor = create_test_executor();
1216
1217 executor
1219 .execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
1220 .unwrap();
1221
1222 let result = executor
1224 .execute("INSERT INTO users (id, name) VALUES (1, 'Alice')")
1225 .unwrap();
1226 assert_eq!(result.rows_affected(), 1);
1227
1228 let mut result = executor.execute("SELECT * FROM users").unwrap();
1230 let columns = result.columns();
1231 assert_eq!(columns.len(), 2);
1232
1233 assert!(result.next());
1234 let row = result.row();
1235 assert_eq!(row.get(0), Some(&Value::Integer(1)));
1236 assert_eq!(row.get(1), Some(&Value::text("Alice")));
1237
1238 assert!(!result.next());
1239 }
1240
1241 #[test]
1242 fn test_parameterized_query() {
1243 let executor = create_test_executor();
1244
1245 executor
1246 .execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
1247 .unwrap();
1248 executor
1249 .execute("INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob')")
1250 .unwrap();
1251
1252 let mut result = executor
1253 .execute_with_params(
1254 "SELECT * FROM users WHERE id = $1",
1255 smallvec::smallvec![Value::Integer(1)],
1256 )
1257 .unwrap();
1258
1259 assert!(result.next());
1260 let row = result.row();
1261 assert_eq!(row.get(0), Some(&Value::Integer(1)));
1262 assert!(!result.next());
1263 }
1264
1265 #[test]
1266 fn test_query_cache_basic() {
1267 let executor = create_test_executor();
1268
1269 executor
1270 .execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
1271 .unwrap();
1272 executor
1273 .execute("INSERT INTO users (id, name) VALUES (1, 'Alice')")
1274 .unwrap();
1275
1276 let stats_before = executor.cache_stats();
1278 executor.execute("SELECT * FROM users").unwrap();
1279 let stats_after = executor.cache_stats();
1280 assert!(stats_after.size > stats_before.size);
1281
1282 let size_before = executor.cache_stats().size;
1284 executor.execute("SELECT * FROM users").unwrap();
1285 let size_after = executor.cache_stats().size;
1286 assert_eq!(size_before, size_after); }
1288
1289 #[test]
1290 fn test_query_cache_parameterized() {
1291 let executor = create_test_executor();
1292
1293 executor
1294 .execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
1295 .unwrap();
1296 executor
1297 .execute("INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob')")
1298 .unwrap();
1299
1300 let query = "SELECT * FROM users WHERE id = $1";
1302
1303 let mut result = executor
1305 .execute_with_params(query, smallvec::smallvec![Value::Integer(1)])
1306 .unwrap();
1307 assert!(result.next());
1308 assert_eq!(result.row().get(0), Some(&Value::Integer(1)));
1309
1310 let mut result = executor
1312 .execute_with_params(query, smallvec::smallvec![Value::Integer(2)])
1313 .unwrap();
1314 assert!(result.next());
1315 assert_eq!(result.row().get(0), Some(&Value::Integer(2)));
1316 }
1317
1318 #[test]
1319 fn test_query_cache_clear() {
1320 let executor = create_test_executor();
1321
1322 executor.execute("SELECT 1").unwrap();
1323 executor.execute("SELECT 2").unwrap();
1324 assert!(executor.cache_stats().size > 0);
1325
1326 executor.clear_cache();
1327 assert_eq!(executor.cache_stats().size, 0);
1328 }
1329
1330 #[test]
1331 fn test_query_cache_uses_exact_source_identity() {
1332 let executor = create_test_executor();
1333
1334 executor.execute("SELECT 1").unwrap();
1335 let size = executor.cache_stats().size;
1336
1337 executor.execute("SELECT 1").unwrap();
1339 assert_eq!(executor.cache_stats().size, size + 1);
1340 }
1341}