Skip to main content

radixdb_executor/
executor.rs

1//! Concrete SQL executor and public execution entry points.
2
3use 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
90/// SQL Query Executor
91///
92/// The executor is the main entry point for executing SQL statements.
93/// It coordinates between the parser, storage engine, and function registry.
94pub struct Executor {
95    /// Storage engine
96    pub(crate) engine: Arc<MVCCEngine>,
97    /// Function registry for scalar, aggregate, and window functions
98    pub(crate) function_registry: Arc<FunctionRegistry>,
99    /// Immutable process startup registry used to admit durable extension bindings.
100    pub(crate) plugin_registry: Arc<PluginRegistry>,
101    /// Query cache for parsed statements
102    pub(crate) query_cache: QueryCache,
103    /// Semantic cache for query results with subsumption detection
104    pub(crate) semantic_cache: Arc<SemanticCache>,
105    /// Cardinality feedback shared by connections to the same engine owner.
106    pub(crate) feedback_cache: Arc<crate::optimizer::FeedbackCache>,
107    /// Rebuildable cache of source-verified procedural programs.
108    pub(crate) procedural_cache: Arc<crate::procedural::ProceduralProgramCache>,
109    /// Active transaction for explicit transaction control (BEGIN/COMMIT/ROLLBACK)
110    pub(crate) active_transaction: Arc<Mutex<Option<ActiveTransaction>>>,
111    /// Default isolation for future transactions created by this SQL
112    /// connection. Forked executors share it only when they are nested work of
113    /// the same connection; independent Database handles never share it.
114    pub(crate) default_isolation_level: Arc<Mutex<radixdb_core::IsolationLevel>>,
115    /// A logical export owns one shared catalog fence for its complete
116    /// lifetime. Its nested read statements must not recursively reacquire the
117    /// same lock, which can deadlock once an exclusive DDL waiter is queued.
118    pub(crate) ddl_fence_already_held: bool,
119    /// Query planner for cost-based optimization (lazily initialized)
120    pub(crate) query_planner: std::sync::OnceLock<QueryPlanner>,
121}
122
123impl Executor {
124    /// Resolve and verify a durable catalog Principal for a network session.
125    /// The returned stable ID is the only identity accepted by later request
126    /// contexts; credentials never leave this boundary.
127    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    /// Fork an executor for nested work while the caller owns a shared catalog
154    /// fence for the complete outer statement/call. Nested SQL must reuse that
155    /// ownership: recursively taking a read lock can deadlock when a writer is
156    /// queued between the outer and inner acquisition.
157    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    /// Resolve a view through the transaction-private catalog when one is
172    /// active, otherwise through the published runtime projection.
173    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    /// Create a new executor with the given storage engine
213    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    /// Create a new executor with a custom function registry
235    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    /// Create a new executor with a custom cache size
260    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    /// Create a connection-local executor with engine-owner-scoped runtime
282    /// caches. Transaction state and parsed-plan cache remain connection-local.
283    #[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    /// Create a connection-local executor with owner-scoped runtime caches and
310    /// the immutable process startup plugin registry.
311    #[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    /// Create a standalone executor against an immutable startup registry.
338    #[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    /// Assess durable package bindings and every external type identity already
363    /// referenced by the database catalog.
364    #[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    /// Construct the executor used by a logical export transaction.
525    ///
526    /// The caller owns a shared [`DdlFenceGuard`] for the executor's complete
527    /// lifetime, so ordinary read statements reuse that catalog generation.
528    #[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    /// Stream one transaction-consistent table snapshot without constructing
544    /// an owning `RowVec` for the whole table.
545    #[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    /// Check if there is an active explicit transaction
565    pub fn has_active_transaction(&self) -> bool {
566        self.active_transaction.lock().unwrap().is_some()
567    }
568
569    /// Return the storage transaction id owned by this executor, if any.
570    #[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    /// Commit the externally installed transaction while retaining the whole
607    /// executor-owned state when a recoverable preflight error leaves it active.
608    #[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    /// Get the query planner (lazily initialized)
638    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    /// Set the default isolation level for new transactions
710    pub fn set_default_isolation_level(&self, level: radixdb_core::IsolationLevel) {
711        *self.default_isolation_level.lock().unwrap() = level;
712    }
713
714    /// Return this connection's default isolation for future transactions.
715    pub fn default_isolation_level(&self) -> radixdb_core::IsolationLevel {
716        *self.default_isolation_level.lock().unwrap()
717    }
718
719    /// Get the storage engine
720    pub fn engine(&self) -> &Arc<MVCCEngine> {
721        &self.engine
722    }
723
724    /// Get the function registry
725    pub fn function_registry(&self) -> &Arc<FunctionRegistry> {
726        &self.function_registry
727    }
728
729    /// Execute a SQL query string
730    ///
731    /// This is the main entry point for executing SQL statements.
732    /// It parses the query and executes each statement in order.
733    /// Uses the query cache to avoid re-parsing identical queries.
734    pub fn execute(&self, sql: &str) -> Result<ExecutionResult> {
735        let ctx = ExecutionContext::new();
736        self.execute_with_context(sql, &ctx)
737    }
738
739    /// Execute a SQL query with positional parameters
740    ///
741    /// Parameters are substituted for $1, $2, etc. placeholders in the query.
742    /// Uses the query cache and selects any eligible borrowed-parameter fast
743    /// path internally, so public facades do not own execution policy.
744    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, &params) {
750                return result;
751            }
752        }
753        let ctx = ExecutionContext::with_params(params);
754        self.execute_with_context(sql, &ctx)
755    }
756
757    /// Try fast path execution with borrowed params slice
758    /// Returns None if fast path doesn't apply, Some(result) otherwise
759    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    /// Execute a SQL query with named parameters
768    ///
769    /// Parameters are substituted for :name placeholders in the query.
770    /// Uses the query cache for efficient re-execution of parameterized queries.
771    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    /// Execute a SQL query with a full execution context
781    /// Uses the query cache for efficient re-execution.
782    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    /// Execute a SQL query using the query cache
798    ///
799    /// This method first checks the cache for a previously parsed statement.
800    /// If found, it uses the cached AST. Otherwise, it parses the query
801    /// and caches the result for future use.
802    fn execute_cached(&self, sql: &str, ctx: &ExecutionContext) -> Result<Box<dyn QueryResult>> {
803        crate::dispatch::program::execute_sql(self, sql, ctx)
804    }
805
806    /// Get the query cache
807    pub fn query_cache(&self) -> &QueryCache {
808        &self.query_cache
809    }
810
811    /// Get query cache statistics
812    pub fn cache_stats(&self) -> CacheStats {
813        self.query_cache.stats()
814    }
815
816    /// Clear the query cache
817    pub fn clear_cache(&self) {
818        self.query_cache.clear();
819        self.procedural_cache.clear();
820    }
821
822    /// Get the semantic cache
823    pub fn semantic_cache(&self) -> &SemanticCache {
824        &self.semantic_cache
825    }
826
827    /// Get semantic cache statistics
828    pub fn semantic_cache_stats(&self) -> SemanticCacheStatsSnapshot {
829        self.semantic_cache.stats()
830    }
831
832    /// Clear the semantic cache
833    pub fn clear_semantic_cache(&self) {
834        self.semantic_cache.clear();
835        self.feedback_cache.clear();
836    }
837
838    /// Invalidate semantic cache for a specific table
839    ///
840    /// Call this after INSERT, UPDATE, DELETE, or TRUNCATE on a table.
841    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    /// Execute a parsed program
847    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    /// Execute a parsed program with context
853    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    /// Execute a single statement
862    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        // A statement that invokes a durable function owns one transaction.
914        // Individual function invocations use savepoints inside it, so a
915        // VOLATILE function never commits independently of its caller. SELECT
916        // retains the boundary until clean cursor exhaustion; eager DML can
917        // complete it as soon as dispatch succeeds.
918        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    /// Install an external storage transaction as the active transaction.
953    ///
954    /// Used by the programmatic Transaction API to delegate SELECT queries
955    /// to the full executor pipeline (aggregates, JOINs, window functions, etc.)
956    /// while keeping the transaction's uncommitted changes visible.
957    #[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    /// Begin a new transaction
974    pub fn begin_transaction(&self) -> Result<Box<dyn Transaction>> {
975        self.engine
976            .begin_transaction_with_level(self.default_isolation_level())
977    }
978
979    /// Begin a new transaction with a specific isolation level
980    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    /// Get or create a cached plan for a SQL statement.
988    ///
989    /// Parses the SQL and caches the plan if not already cached.
990    /// Returns a lightweight CachedPlanRef that can be stored and reused
991    /// for repeated execution without re-parsing or cache lookup overhead.
992    pub fn get_or_create_plan(&self, sql: &str) -> Result<CachedPlanRef> {
993        crate::dispatch::program::get_or_create_plan(self, sql)
994    }
995
996    /// Execute a pre-cached plan directly, skipping cache lookup.
997    ///
998    /// This is the fast path for prepared statements: the caller holds a
999    /// `CachedPlanRef` obtained from `get_or_create_plan()` and passes it
1000    /// here on every execution, avoiding normalize + hash + RwLock read
1001    /// per call.
1002    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        // Cached/compiled paths are execution accelerators, never an
1034        // authorization authority. Check before any SELECT/DML fast path;
1035        // fallback statement dispatch intentionally checks again on its own
1036        // immutable catalog generation.
1037        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        // Try compiled fast paths based on statement type
1049        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/// Count the number of parameter placeholders in a statement
1170///
1171/// Returns (has_params, max_param_index)
1172#[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        // Create table
1218        executor
1219            .execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
1220            .unwrap();
1221
1222        // Insert data
1223        let result = executor
1224            .execute("INSERT INTO users (id, name) VALUES (1, 'Alice')")
1225            .unwrap();
1226        assert_eq!(result.rows_affected(), 1);
1227
1228        // Select data
1229        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        // First execution - should parse and cache
1277        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        // Second execution - should use cache
1283        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); // No new entries
1287    }
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        // Execute with different parameters - should reuse cached plan
1301        let query = "SELECT * FROM users WHERE id = $1";
1302
1303        // First execution
1304        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        // Second execution with different param - should use cache
1311        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        // Distinct source text gets a distinct key unless normalization is lexical.
1338        executor.execute("SELECT 1").unwrap();
1339        assert_eq!(executor.cache_stats().size, size + 1);
1340    }
1341}