Skip to main content

alopex_sql/executor/
mod.rs

1//! SQL Executor module for Alopex SQL.
2//!
3//! This module provides the execution engine for SQL statements.
4//!
5//! # Overview
6//!
7//! The Executor takes a [`LogicalPlan`] from the Planner and executes it
8//! against the storage layer. It supports DDL, DML, and Query operations.
9//!
10//! Query execution currently materializes intermediate results per stage;
11//! future versions may add streaming pipelines as requirements grow.
12
13//! # Components
14//!
15//! - [`Executor`]: Main executor struct
16//! - [`ExecutorError`]: Error types for execution
17//! - [`ExecutionResult`]: Execution result types
18//!
19//! # Example
20//!
21//! ```ignore
22//! use std::sync::{Arc, RwLock};
23//! use alopex_core::kv::memory::MemoryKV;
24//! use alopex_sql::executor::Executor;
25//! use alopex_sql::catalog::MemoryCatalog;
26//! use alopex_sql::planner::LogicalPlan;
27//!
28//! // Create storage and catalog
29//! let store = Arc::new(MemoryKV::new());
30//! let catalog = Arc::new(RwLock::new(MemoryCatalog::new()));
31//!
32//! // Create executor
33//! let mut executor = Executor::new(store, catalog);
34//!
35//! // Execute a plan
36//! let result = executor.execute(plan)?;
37//! ```
38
39#[cfg(feature = "tokio")]
40pub mod async_executor;
41pub mod bulk;
42pub(crate) mod ddl;
43pub(crate) mod dml;
44mod error;
45pub mod evaluator;
46mod hnsw_bridge;
47pub mod memory;
48pub mod query;
49mod result;
50mod system;
51
52#[cfg(feature = "tokio")]
53pub use async_executor::AsyncExecutor;
54pub use error::{ConstraintViolation, EvaluationError, ExecutorError, Result};
55pub use memory::{MemoryPolicy, SpillPolicy};
56pub use query::{RowIterator, ScanIterator, build_streaming_pipeline};
57pub use result::{ColumnInfo, ExecutionResult, QueryResult, QueryRowIterator, Row};
58
59/// Returns whether a plan requires direct access to the backing KV store.
60pub fn is_store_direct_plan(plan: &LogicalPlan) -> bool {
61    system::is_store_direct_plan(plan)
62}
63
64use std::sync::{Arc, RwLock};
65
66use alopex_core::kv::KVStore;
67use alopex_core::types::TxnMode;
68
69use crate::catalog::Catalog;
70use crate::catalog::persistent::{IndexFqn, TableFqn};
71use crate::catalog::{CatalogError, CatalogOverlay, PersistentCatalog, TxnCatalogView};
72use crate::planner::LogicalPlan;
73use crate::storage::{BorrowedSqlTransaction, KeyEncoder, SqlTransaction, SqlTxn as _, TxnBridge};
74
75/// SQL statement executor.
76///
77/// The Executor takes a [`LogicalPlan`] and executes it against the storage layer.
78/// It manages transactions and coordinates between DDL, DML, and Query operations.
79///
80/// # Type Parameters
81///
82/// - `S`: The underlying KV store type (must implement [`KVStore`])
83/// - `C`: The catalog type (must implement [`Catalog`])
84pub struct Executor<S: KVStore, C: Catalog> {
85    /// Transaction bridge for storage operations.
86    bridge: TxnBridge<S>,
87
88    /// Catalog for metadata operations.
89    catalog: Arc<RwLock<C>>,
90}
91
92impl<S: KVStore, C: Catalog> Executor<S, C> {
93    fn run_in_write_txn<R, F>(&self, f: F) -> Result<R>
94    where
95        F: FnOnce(&mut SqlTransaction<'_, S>) -> Result<R>,
96    {
97        let mut txn = self.bridge.begin_write().map_err(ExecutorError::from)?;
98        match f(&mut txn) {
99            Ok(result) => {
100                txn.commit().map_err(ExecutorError::from)?;
101                Ok(result)
102            }
103            Err(err) => {
104                txn.rollback().map_err(ExecutorError::from)?;
105                Err(err)
106            }
107        }
108    }
109
110    /// Create a new Executor with the given store and catalog.
111    ///
112    /// # Arguments
113    ///
114    /// - `store`: The underlying KV store
115    /// - `catalog`: The catalog for metadata operations
116    pub fn new(store: Arc<S>, catalog: Arc<RwLock<C>>) -> Self {
117        Self {
118            bridge: TxnBridge::new(store),
119            catalog,
120        }
121    }
122
123    /// Execute a logical plan and return the result.
124    ///
125    /// # Arguments
126    ///
127    /// - `plan`: The logical plan to execute
128    ///
129    /// # Returns
130    ///
131    /// Returns an [`ExecutionResult`] on success, or an [`ExecutorError`] on failure.
132    ///
133    /// # DDL Operations
134    ///
135    /// - `CreateTable`: Creates a new table with optional PK index
136    /// - `DropTable`: Drops a table and its associated indexes
137    /// - `CreateIndex`: Creates a new index
138    /// - `DropIndex`: Drops an index
139    ///
140    /// # DML Operations
141    ///
142    /// - `Insert`: Inserts rows into a table
143    /// - `Update`: Updates rows in a table
144    /// - `Delete`: Deletes rows from a table
145    ///
146    /// # Query Operations
147    ///
148    /// - `Scan`, `Filter`, `Sort`, `Limit`: SELECT query execution
149    pub fn execute(&mut self, plan: LogicalPlan) -> Result<ExecutionResult> {
150        match plan {
151            LogicalPlan::Pragma { name, value } => {
152                system::execute_pragma(&self.bridge, &name, value.as_ref())
153            }
154            // DDL Operations
155            LogicalPlan::CreateTable {
156                table,
157                if_not_exists,
158                with_options,
159            } => self.execute_create_table(table, with_options, if_not_exists),
160            LogicalPlan::DropTable { name, if_exists } => self.execute_drop_table(&name, if_exists),
161            LogicalPlan::CreateIndex {
162                index,
163                if_not_exists,
164            } => self.execute_create_index(index, if_not_exists),
165            LogicalPlan::DropIndex { name, if_exists } => self.execute_drop_index(&name, if_exists),
166
167            // DML Operations
168            LogicalPlan::Insert {
169                table,
170                columns,
171                values,
172            } => self.execute_insert(&table, columns, values),
173            LogicalPlan::Update {
174                table,
175                assignments,
176                filter,
177            } => self.execute_update(&table, assignments, filter),
178            LogicalPlan::Delete { table, filter } => self.execute_delete(&table, filter),
179
180            // Query Operations
181            LogicalPlan::Scan { .. }
182            | LogicalPlan::Filter { .. }
183            | LogicalPlan::Project { .. }
184            | LogicalPlan::Join { .. }
185            | LogicalPlan::Aggregate { .. }
186            | LogicalPlan::Sort { .. }
187            | LogicalPlan::Limit { .. } => self.execute_query(plan),
188        }
189    }
190
191    // ========================================================================
192    // DDL Operations (to be implemented in Phase 2)
193    // ========================================================================
194
195    fn execute_create_table(
196        &mut self,
197        table: crate::catalog::TableMetadata,
198        with_options: Vec<(String, String)>,
199        if_not_exists: bool,
200    ) -> Result<ExecutionResult> {
201        let mut catalog = self.catalog.write().expect("catalog lock poisoned");
202        self.run_in_write_txn(|txn| {
203            ddl::create_table::execute_create_table(
204                txn,
205                &mut *catalog,
206                table,
207                with_options,
208                if_not_exists,
209            )
210        })
211    }
212
213    fn execute_drop_table(&mut self, name: &str, if_exists: bool) -> Result<ExecutionResult> {
214        let mut catalog = self.catalog.write().expect("catalog lock poisoned");
215        self.run_in_write_txn(|txn| {
216            ddl::drop_table::execute_drop_table(txn, &mut *catalog, name, if_exists)
217        })
218    }
219
220    fn execute_create_index(
221        &mut self,
222        index: crate::catalog::IndexMetadata,
223        if_not_exists: bool,
224    ) -> Result<ExecutionResult> {
225        let mut catalog = self.catalog.write().expect("catalog lock poisoned");
226        self.run_in_write_txn(|txn| {
227            ddl::create_index::execute_create_index(txn, &mut *catalog, index, if_not_exists)
228        })
229    }
230
231    fn execute_drop_index(&mut self, name: &str, if_exists: bool) -> Result<ExecutionResult> {
232        let mut catalog = self.catalog.write().expect("catalog lock poisoned");
233        self.run_in_write_txn(|txn| {
234            ddl::drop_index::execute_drop_index(txn, &mut *catalog, name, if_exists)
235        })
236    }
237
238    // ========================================================================
239    // DML Operations (implemented in Phase 4)
240    // ========================================================================
241
242    fn execute_insert(
243        &mut self,
244        table: &str,
245        columns: Vec<String>,
246        values: Vec<Vec<crate::planner::TypedExpr>>,
247    ) -> Result<ExecutionResult> {
248        let catalog = self.catalog.read().expect("catalog lock poisoned");
249        self.run_in_write_txn(|txn| dml::execute_insert(txn, &*catalog, table, columns, values))
250    }
251
252    fn execute_update(
253        &mut self,
254        table: &str,
255        assignments: Vec<crate::planner::TypedAssignment>,
256        filter: Option<crate::planner::TypedExpr>,
257    ) -> Result<ExecutionResult> {
258        let catalog = self.catalog.read().expect("catalog lock poisoned");
259        self.run_in_write_txn(|txn| dml::execute_update(txn, &*catalog, table, assignments, filter))
260    }
261
262    fn execute_delete(
263        &mut self,
264        table: &str,
265        filter: Option<crate::planner::TypedExpr>,
266    ) -> Result<ExecutionResult> {
267        let catalog = self.catalog.read().expect("catalog lock poisoned");
268        self.run_in_write_txn(|txn| dml::execute_delete(txn, &*catalog, table, filter))
269    }
270
271    // ========================================================================
272    // Query Operations (to be implemented in Phase 5)
273    // ========================================================================
274
275    fn execute_query(&mut self, plan: LogicalPlan) -> Result<ExecutionResult> {
276        if let Some(result) = system::try_execute(&self.bridge, &plan)? {
277            return Ok(result);
278        }
279        let catalog = self.catalog.read().expect("catalog lock poisoned");
280        self.run_in_write_txn(|txn| query::execute_query(txn, &*catalog, plan))
281    }
282}
283
284impl<S: KVStore> Executor<S, PersistentCatalog<S>> {
285    pub fn execute_in_txn<'a, 'b, 'c>(
286        &mut self,
287        plan: LogicalPlan,
288        txn: &mut BorrowedSqlTransaction<'a, 'b, 'c, S>,
289    ) -> Result<ExecutionResult> {
290        if txn.mode() == TxnMode::ReadOnly
291            && !matches!(
292                plan,
293                LogicalPlan::Scan { .. }
294                    | LogicalPlan::Filter { .. }
295                    | LogicalPlan::Project { .. }
296                    | LogicalPlan::Join { .. }
297                    | LogicalPlan::Aggregate { .. }
298                    | LogicalPlan::Sort { .. }
299                    | LogicalPlan::Limit { .. }
300            )
301        {
302            return Err(ExecutorError::ReadOnlyTransaction {
303                operation: plan.operation_name().to_string(),
304            });
305        }
306
307        let mut catalog = self.catalog.write().expect("catalog lock poisoned");
308        let (mut sql_txn, overlay) = txn.split_parts();
309
310        let result = match plan {
311            LogicalPlan::CreateTable {
312                table,
313                if_not_exists,
314                with_options,
315            } => self.execute_create_table_in_txn(
316                &mut *catalog,
317                &mut sql_txn,
318                overlay,
319                table,
320                with_options,
321                if_not_exists,
322            ),
323            LogicalPlan::DropTable { name, if_exists } => self.execute_drop_table_in_txn(
324                &mut *catalog,
325                &mut sql_txn,
326                overlay,
327                &name,
328                if_exists,
329            ),
330            LogicalPlan::CreateIndex {
331                index,
332                if_not_exists,
333            } => self.execute_create_index_in_txn(
334                &mut *catalog,
335                &mut sql_txn,
336                overlay,
337                index,
338                if_not_exists,
339            ),
340            LogicalPlan::DropIndex { name, if_exists } => self.execute_drop_index_in_txn(
341                &mut *catalog,
342                &mut sql_txn,
343                overlay,
344                &name,
345                if_exists,
346            ),
347            LogicalPlan::Pragma { .. } => Err(ExecutorError::UnsupportedOperation(
348                "PRAGMA is not available inside an external transaction".to_string(),
349            )),
350            LogicalPlan::Insert {
351                table,
352                columns,
353                values,
354            } => {
355                let view = TxnCatalogView::new(&*catalog, &*overlay);
356                dml::execute_insert(&mut sql_txn, &view, &table, columns, values)
357            }
358            LogicalPlan::Update {
359                table,
360                assignments,
361                filter,
362            } => {
363                let view = TxnCatalogView::new(&*catalog, &*overlay);
364                dml::execute_update(&mut sql_txn, &view, &table, assignments, filter)
365            }
366            LogicalPlan::Delete { table, filter } => {
367                let view = TxnCatalogView::new(&*catalog, &*overlay);
368                dml::execute_delete(&mut sql_txn, &view, &table, filter)
369            }
370            LogicalPlan::Scan { .. }
371            | LogicalPlan::Filter { .. }
372            | LogicalPlan::Project { .. }
373            | LogicalPlan::Join { .. }
374            | LogicalPlan::Aggregate { .. }
375            | LogicalPlan::Sort { .. }
376            | LogicalPlan::Limit { .. } => {
377                let view = TxnCatalogView::new(&*catalog, &*overlay);
378                query::execute_query(&mut sql_txn, &view, plan)
379            }
380        };
381
382        match result {
383            Ok(value) => {
384                sql_txn.flush_hnsw()?;
385                Ok(value)
386            }
387            Err(err) => {
388                let _ = sql_txn.abandon_hnsw();
389                Err(err)
390            }
391        }
392    }
393
394    fn map_catalog_error(err: CatalogError) -> ExecutorError {
395        match err {
396            CatalogError::Kv(e) => ExecutorError::Core(e),
397            CatalogError::Serialize(e) => ExecutorError::InvalidOperation {
398                operation: "CatalogPersistence".into(),
399                reason: e.to_string(),
400            },
401            CatalogError::InvalidKey(reason) => ExecutorError::InvalidOperation {
402                operation: "CatalogPersistence".into(),
403                reason,
404            },
405        }
406    }
407
408    fn execute_create_table_in_txn<'txn>(
409        &self,
410        catalog: &mut PersistentCatalog<S>,
411        txn: &mut impl crate::storage::SqlTxn<'txn, S>,
412        overlay: &mut CatalogOverlay,
413        mut table: crate::catalog::TableMetadata,
414        with_options: Vec<(String, String)>,
415        if_not_exists: bool,
416    ) -> Result<ExecutionResult>
417    where
418        S: 'txn,
419    {
420        if catalog.table_exists_in_txn(&table.name, overlay) {
421            return if if_not_exists {
422                Ok(ExecutionResult::Success)
423            } else {
424                Err(ExecutorError::TableAlreadyExists(table.name))
425            };
426        }
427
428        table.storage_options = ddl::create_table::parse_storage_options(&with_options)?;
429
430        let pk_index = if let Some(pk_columns) = table.primary_key.clone() {
431            let column_indices = pk_columns
432                .iter()
433                .map(|name| {
434                    table
435                        .get_column_index(name)
436                        .ok_or_else(|| ExecutorError::ColumnNotFound(name.clone()))
437                })
438                .collect::<Result<Vec<_>>>()?;
439            let index_id = catalog.next_index_id();
440            let index_name = ddl::create_pk_index_name(&table.name);
441            let mut index = crate::catalog::IndexMetadata::new(
442                index_id,
443                index_name,
444                table.name.clone(),
445                pk_columns,
446            )
447            .with_column_indices(column_indices)
448            .with_unique(true);
449            index.catalog_name = table.catalog_name.clone();
450            index.namespace_name = table.namespace_name.clone();
451            Some(index)
452        } else {
453            None
454        };
455
456        let table_id = catalog.next_table_id();
457        table = table.with_table_id(table_id);
458
459        // storage keyspace の初期化
460        txn.delete_prefix(&KeyEncoder::table_prefix(table_id))?;
461        txn.delete_prefix(&KeyEncoder::sequence_key(table_id))?;
462
463        // 永続化(同一 KV トランザクション内)
464        catalog
465            .persist_create_table(txn.inner_mut(), &table)
466            .map_err(Self::map_catalog_error)?;
467        if let Some(index) = &pk_index {
468            catalog
469                .persist_create_index(txn.inner_mut(), index)
470                .map_err(Self::map_catalog_error)?;
471        }
472
473        // オーバーレイに反映(ベースカタログはコミットまで不変)
474        overlay.add_table(TableFqn::from(&table), table);
475        if let Some(index) = pk_index {
476            overlay.add_index(IndexFqn::from(&index), index);
477        }
478
479        Ok(ExecutionResult::Success)
480    }
481
482    fn execute_drop_table_in_txn<'txn>(
483        &self,
484        catalog: &mut PersistentCatalog<S>,
485        txn: &mut impl crate::storage::SqlTxn<'txn, S>,
486        overlay: &mut CatalogOverlay,
487        table_name: &str,
488        if_exists: bool,
489    ) -> Result<ExecutionResult>
490    where
491        S: 'txn,
492    {
493        let table_meta = match catalog.get_table_in_txn(table_name, overlay) {
494            Some(table) => table.clone(),
495            None => {
496                return if if_exists {
497                    Ok(ExecutionResult::Success)
498                } else {
499                    Err(ExecutorError::TableNotFound(table_name.to_string()))
500                };
501            }
502        };
503        if table_meta.catalog_name != "default" || table_meta.namespace_name != "default" {
504            return if if_exists {
505                Ok(ExecutionResult::Success)
506            } else {
507                Err(ExecutorError::TableNotFound(table_name.to_string()))
508            };
509        }
510
511        let indexes = TxnCatalogView::new(catalog, overlay)
512            .get_indexes_for_table(table_name)
513            .into_iter()
514            .cloned()
515            .collect::<Vec<_>>();
516
517        for index in &indexes {
518            if matches!(index.method, Some(crate::ast::ddl::IndexMethod::Hnsw)) {
519                crate::executor::hnsw_bridge::HnswBridge::drop_index(txn, index, false)?;
520            } else {
521                txn.delete_prefix(&KeyEncoder::index_prefix(index.index_id))?;
522            }
523        }
524
525        txn.delete_prefix(&KeyEncoder::table_prefix(table_meta.table_id))?;
526        txn.delete_prefix(&KeyEncoder::sequence_key(table_meta.table_id))?;
527
528        catalog
529            .persist_drop_table(txn.inner_mut(), &TableFqn::from(&table_meta))
530            .map_err(Self::map_catalog_error)?;
531
532        overlay.drop_table(&TableFqn::from(&table_meta));
533
534        Ok(ExecutionResult::Success)
535    }
536
537    fn execute_create_index_in_txn<'txn>(
538        &self,
539        catalog: &mut PersistentCatalog<S>,
540        txn: &mut impl crate::storage::SqlTxn<'txn, S>,
541        overlay: &mut CatalogOverlay,
542        mut index: crate::catalog::IndexMetadata,
543        if_not_exists: bool,
544    ) -> Result<ExecutionResult>
545    where
546        S: 'txn,
547    {
548        if ddl::is_implicit_pk_index(&index.name) {
549            return Err(ExecutorError::InvalidIndexName {
550                name: index.name.clone(),
551                reason: "Index names starting with '__pk_' are reserved for PRIMARY KEY".into(),
552            });
553        }
554
555        if catalog.index_exists_in_txn(&index.name, overlay) {
556            return if if_not_exists {
557                Ok(ExecutionResult::Success)
558            } else {
559                Err(ExecutorError::IndexAlreadyExists(index.name))
560            };
561        }
562
563        let table = catalog
564            .get_table_in_txn(&index.table, overlay)
565            .ok_or_else(|| ExecutorError::TableNotFound(index.table.clone()))?
566            .clone();
567        index.catalog_name = table.catalog_name.clone();
568        index.namespace_name = table.namespace_name.clone();
569
570        let column_indices = index
571            .columns
572            .iter()
573            .map(|name| {
574                table
575                    .get_column_index(name)
576                    .ok_or_else(|| ExecutorError::ColumnNotFound(name.clone()))
577            })
578            .collect::<Result<Vec<_>>>()?;
579
580        let index_id = catalog.next_index_id();
581        index.index_id = index_id;
582        index.column_indices = column_indices.clone();
583
584        if matches!(index.method, Some(crate::ast::ddl::IndexMethod::Hnsw)) {
585            crate::executor::hnsw_bridge::HnswBridge::create_index(txn, &table, &index)?;
586        } else {
587            ddl::create_index::build_index_for_existing_rows(txn, &table, &index, column_indices)?;
588        }
589
590        catalog
591            .persist_create_index(txn.inner_mut(), &index)
592            .map_err(Self::map_catalog_error)?;
593
594        overlay.add_index(IndexFqn::from(&index), index);
595
596        Ok(ExecutionResult::Success)
597    }
598
599    fn execute_drop_index_in_txn<'txn>(
600        &self,
601        catalog: &mut PersistentCatalog<S>,
602        txn: &mut impl crate::storage::SqlTxn<'txn, S>,
603        overlay: &mut CatalogOverlay,
604        index_name: &str,
605        if_exists: bool,
606    ) -> Result<ExecutionResult>
607    where
608        S: 'txn,
609    {
610        if ddl::is_implicit_pk_index(index_name) {
611            return Err(ExecutorError::InvalidOperation {
612                operation: "DROP INDEX".into(),
613                reason: "Cannot drop implicit PRIMARY KEY index directly; use DROP TABLE".into(),
614            });
615        }
616
617        let index = match catalog.get_index_in_txn(index_name, overlay) {
618            Some(index) => index.clone(),
619            None => {
620                return if if_exists {
621                    Ok(ExecutionResult::Success)
622                } else {
623                    Err(ExecutorError::IndexNotFound(index_name.to_string()))
624                };
625            }
626        };
627        if index.catalog_name != "default" || index.namespace_name != "default" {
628            return if if_exists {
629                Ok(ExecutionResult::Success)
630            } else {
631                Err(ExecutorError::IndexNotFound(index_name.to_string()))
632            };
633        }
634
635        if matches!(index.method, Some(crate::ast::ddl::IndexMethod::Hnsw)) {
636            crate::executor::hnsw_bridge::HnswBridge::drop_index(txn, &index, if_exists)?;
637        } else {
638            txn.delete_prefix(&KeyEncoder::index_prefix(index.index_id))?;
639        }
640
641        catalog
642            .persist_drop_index(txn.inner_mut(), &IndexFqn::from(&index))
643            .map_err(Self::map_catalog_error)?;
644
645        overlay.drop_index(&IndexFqn::from(&index));
646
647        Ok(ExecutionResult::Success)
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654    use crate::catalog::MemoryCatalog;
655    use alopex_core::kv::memory::MemoryKV;
656
657    fn create_executor() -> Executor<MemoryKV, MemoryCatalog> {
658        let store = Arc::new(MemoryKV::new());
659        let catalog = Arc::new(RwLock::new(MemoryCatalog::new()));
660        Executor::new(store, catalog)
661    }
662
663    #[test]
664    fn test_executor_creation() {
665        let _executor = create_executor();
666        // Executor should be created without panic
667    }
668
669    #[test]
670    fn create_table_is_supported() {
671        let mut executor = create_executor();
672
673        use crate::catalog::{ColumnMetadata, TableMetadata};
674        use crate::planner::ResolvedType;
675
676        let table = TableMetadata::new(
677            "test",
678            vec![ColumnMetadata::new("id", ResolvedType::Integer)],
679        );
680
681        let result = executor.execute(LogicalPlan::CreateTable {
682            table,
683            if_not_exists: false,
684            with_options: vec![],
685        });
686        assert!(matches!(result, Ok(ExecutionResult::Success)));
687
688        let catalog = executor.catalog.read().unwrap();
689        assert!(catalog.table_exists("test"));
690    }
691
692    #[test]
693    fn insert_is_supported() {
694        use crate::Span;
695        use crate::catalog::{ColumnMetadata, TableMetadata};
696        use crate::planner::typed_expr::TypedExprKind;
697        use crate::planner::types::ResolvedType;
698
699        let mut executor = create_executor();
700
701        let table = TableMetadata::new("t", vec![ColumnMetadata::new("id", ResolvedType::Integer)])
702            .with_primary_key(vec!["id".into()]);
703
704        executor
705            .execute(LogicalPlan::CreateTable {
706                table,
707                if_not_exists: false,
708                with_options: vec![],
709            })
710            .unwrap();
711
712        let result = executor.execute(LogicalPlan::Insert {
713            table: "t".into(),
714            columns: vec!["id".into()],
715            values: vec![vec![crate::planner::typed_expr::TypedExpr {
716                kind: TypedExprKind::Literal(crate::ast::expr::Literal::Number("1".into())),
717                resolved_type: ResolvedType::Integer,
718                span: Span::default(),
719            }]],
720        });
721        assert!(matches!(result, Ok(ExecutionResult::RowsAffected(1))));
722    }
723
724    #[test]
725    fn system_pragma_and_stats_function_use_the_store() {
726        let mut executor = create_executor();
727        let catalog = MemoryCatalog::new();
728
729        let pragma = crate::Parser::parse_sql(&crate::AlopexDialect, "PRAGMA cache_size = 8")
730            .unwrap()
731            .pop()
732            .unwrap();
733        let plan = crate::Planner::new(&catalog).plan(&pragma).unwrap();
734        assert!(matches!(
735            executor.execute(plan),
736            Ok(ExecutionResult::Success)
737        ));
738
739        let select = crate::Parser::parse_sql(&crate::AlopexDialect, "SELECT memory_stats()")
740            .unwrap()
741            .pop()
742            .unwrap();
743        let plan = crate::Planner::new(&catalog).plan(&select).unwrap();
744        let result = executor.execute(plan).unwrap();
745        let ExecutionResult::Query(result) = result else {
746            panic!("expected query result");
747        };
748        assert_eq!(result.columns[0].name, "memory_stats");
749        assert!(
750            matches!(&result.rows[0][0], crate::SqlValue::Text(text) if text.contains("total_bytes"))
751        );
752    }
753}