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        let _statement_timestamp = evaluator::begin_statement();
151        match plan {
152            LogicalPlan::Pragma { name, value } => {
153                system::execute_pragma(&self.bridge, &name, value.as_ref())
154            }
155            // DDL Operations
156            LogicalPlan::CreateTable {
157                table,
158                if_not_exists,
159                with_options,
160            } => self.execute_create_table(table, with_options, if_not_exists),
161            LogicalPlan::DropTable { name, if_exists } => self.execute_drop_table(&name, if_exists),
162            LogicalPlan::CreateIndex {
163                index,
164                if_not_exists,
165            } => self.execute_create_index(index, if_not_exists),
166            LogicalPlan::DropIndex { name, if_exists } => self.execute_drop_index(&name, if_exists),
167
168            // DML Operations
169            LogicalPlan::Insert {
170                table,
171                columns,
172                values,
173            } => self.execute_insert(&table, columns, values),
174            LogicalPlan::InsertSelect {
175                table,
176                columns,
177                source,
178            } => self.execute_insert_select(&table, columns, *source),
179            LogicalPlan::Update {
180                table,
181                assignments,
182                filter,
183            } => self.execute_update(&table, assignments, filter),
184            LogicalPlan::Delete { table, filter } => self.execute_delete(&table, filter),
185
186            // Query Operations
187            LogicalPlan::Scan { .. }
188            | LogicalPlan::Filter { .. }
189            | LogicalPlan::Project { .. }
190            | LogicalPlan::Join { .. }
191            | LogicalPlan::Aggregate { .. }
192            | LogicalPlan::Window { .. }
193            | LogicalPlan::SetOperation { .. }
194            | LogicalPlan::RecursiveCte { .. }
195            | LogicalPlan::RecursiveReference { .. }
196            | LogicalPlan::Sort { .. }
197            | LogicalPlan::Limit { .. } => self.execute_query(plan),
198        }
199    }
200
201    // ========================================================================
202    // DDL Operations (to be implemented in Phase 2)
203    // ========================================================================
204
205    fn execute_create_table(
206        &mut self,
207        table: crate::catalog::TableMetadata,
208        with_options: Vec<(String, String)>,
209        if_not_exists: bool,
210    ) -> Result<ExecutionResult> {
211        let mut catalog = self.catalog.write().expect("catalog lock poisoned");
212        self.run_in_write_txn(|txn| {
213            ddl::create_table::execute_create_table(
214                txn,
215                &mut *catalog,
216                table,
217                with_options,
218                if_not_exists,
219            )
220        })
221    }
222
223    fn execute_drop_table(&mut self, name: &str, if_exists: bool) -> Result<ExecutionResult> {
224        let mut catalog = self.catalog.write().expect("catalog lock poisoned");
225        self.run_in_write_txn(|txn| {
226            ddl::drop_table::execute_drop_table(txn, &mut *catalog, name, if_exists)
227        })
228    }
229
230    fn execute_create_index(
231        &mut self,
232        index: crate::catalog::IndexMetadata,
233        if_not_exists: bool,
234    ) -> Result<ExecutionResult> {
235        let mut catalog = self.catalog.write().expect("catalog lock poisoned");
236        self.run_in_write_txn(|txn| {
237            ddl::create_index::execute_create_index(txn, &mut *catalog, index, if_not_exists)
238        })
239    }
240
241    fn execute_drop_index(&mut self, name: &str, if_exists: bool) -> Result<ExecutionResult> {
242        let mut catalog = self.catalog.write().expect("catalog lock poisoned");
243        self.run_in_write_txn(|txn| {
244            ddl::drop_index::execute_drop_index(txn, &mut *catalog, name, if_exists)
245        })
246    }
247
248    // ========================================================================
249    // DML Operations (implemented in Phase 4)
250    // ========================================================================
251
252    fn execute_insert(
253        &mut self,
254        table: &str,
255        columns: Vec<String>,
256        values: Vec<Vec<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_insert(txn, &*catalog, table, columns, values))
260    }
261
262    fn execute_insert_select(
263        &mut self,
264        table: &str,
265        columns: Vec<String>,
266        source: LogicalPlan,
267    ) -> Result<ExecutionResult> {
268        let catalog = self.catalog.read().expect("catalog lock poisoned");
269        self.run_in_write_txn(|txn| {
270            let ExecutionResult::Query(result) = query::execute_query(txn, &*catalog, source)?
271            else {
272                return Err(ExecutorError::InvalidOperation {
273                    operation: "INSERT ... SELECT".into(),
274                    reason: "SELECT source did not return query rows".into(),
275                });
276            };
277            dml::execute_insert_rows(txn, &*catalog, table, columns, result.rows)
278        })
279    }
280
281    fn execute_update(
282        &mut self,
283        table: &str,
284        assignments: Vec<crate::planner::TypedAssignment>,
285        filter: Option<crate::planner::TypedExpr>,
286    ) -> Result<ExecutionResult> {
287        let catalog = self.catalog.read().expect("catalog lock poisoned");
288        self.run_in_write_txn(|txn| dml::execute_update(txn, &*catalog, table, assignments, filter))
289    }
290
291    fn execute_delete(
292        &mut self,
293        table: &str,
294        filter: Option<crate::planner::TypedExpr>,
295    ) -> Result<ExecutionResult> {
296        let catalog = self.catalog.read().expect("catalog lock poisoned");
297        self.run_in_write_txn(|txn| dml::execute_delete(txn, &*catalog, table, filter))
298    }
299
300    // ========================================================================
301    // Query Operations (to be implemented in Phase 5)
302    // ========================================================================
303
304    fn execute_query(&mut self, plan: LogicalPlan) -> Result<ExecutionResult> {
305        if let Some(result) = system::try_execute(&self.bridge, &plan)? {
306            return Ok(result);
307        }
308        let catalog = self.catalog.read().expect("catalog lock poisoned");
309        self.run_in_write_txn(|txn| query::execute_query(txn, &*catalog, plan))
310    }
311}
312
313impl<S: KVStore> Executor<S, PersistentCatalog<S>> {
314    pub fn execute_in_txn<'a, 'b, 'c>(
315        &mut self,
316        plan: LogicalPlan,
317        txn: &mut BorrowedSqlTransaction<'a, 'b, 'c, S>,
318    ) -> Result<ExecutionResult> {
319        if txn.mode() == TxnMode::ReadOnly
320            && !matches!(
321                plan,
322                LogicalPlan::Scan { .. }
323                    | LogicalPlan::Filter { .. }
324                    | LogicalPlan::Project { .. }
325                    | LogicalPlan::Join { .. }
326                    | LogicalPlan::Aggregate { .. }
327                    | LogicalPlan::Window { .. }
328                    | LogicalPlan::SetOperation { .. }
329                    | LogicalPlan::RecursiveCte { .. }
330                    | LogicalPlan::RecursiveReference { .. }
331                    | LogicalPlan::Sort { .. }
332                    | LogicalPlan::Limit { .. }
333            )
334        {
335            return Err(ExecutorError::ReadOnlyTransaction {
336                operation: plan.operation_name().to_string(),
337            });
338        }
339
340        let _statement_timestamp = evaluator::begin_statement();
341        let mut catalog = self.catalog.write().expect("catalog lock poisoned");
342        let (mut sql_txn, overlay) = txn.split_parts();
343
344        let result = match plan {
345            LogicalPlan::CreateTable {
346                table,
347                if_not_exists,
348                with_options,
349            } => self.execute_create_table_in_txn(
350                &mut *catalog,
351                &mut sql_txn,
352                overlay,
353                table,
354                with_options,
355                if_not_exists,
356            ),
357            LogicalPlan::DropTable { name, if_exists } => self.execute_drop_table_in_txn(
358                &mut *catalog,
359                &mut sql_txn,
360                overlay,
361                &name,
362                if_exists,
363            ),
364            LogicalPlan::CreateIndex {
365                index,
366                if_not_exists,
367            } => self.execute_create_index_in_txn(
368                &mut *catalog,
369                &mut sql_txn,
370                overlay,
371                index,
372                if_not_exists,
373            ),
374            LogicalPlan::DropIndex { name, if_exists } => self.execute_drop_index_in_txn(
375                &mut *catalog,
376                &mut sql_txn,
377                overlay,
378                &name,
379                if_exists,
380            ),
381            LogicalPlan::Pragma { .. } => Err(ExecutorError::UnsupportedOperation(
382                "PRAGMA is not available inside an external transaction".to_string(),
383            )),
384            LogicalPlan::Insert {
385                table,
386                columns,
387                values,
388            } => {
389                let view = TxnCatalogView::new(&*catalog, &*overlay);
390                dml::execute_insert(&mut sql_txn, &view, &table, columns, values)
391            }
392            LogicalPlan::InsertSelect {
393                table,
394                columns,
395                source,
396            } => {
397                let view = TxnCatalogView::new(&*catalog, &*overlay);
398                let ExecutionResult::Query(result) =
399                    query::execute_query(&mut sql_txn, &view, *source)?
400                else {
401                    return Err(ExecutorError::InvalidOperation {
402                        operation: "INSERT ... SELECT".into(),
403                        reason: "SELECT source did not return query rows".into(),
404                    });
405                };
406                dml::execute_insert_rows(&mut sql_txn, &view, &table, columns, result.rows)
407            }
408            LogicalPlan::Update {
409                table,
410                assignments,
411                filter,
412            } => {
413                let view = TxnCatalogView::new(&*catalog, &*overlay);
414                dml::execute_update(&mut sql_txn, &view, &table, assignments, filter)
415            }
416            LogicalPlan::Delete { table, filter } => {
417                let view = TxnCatalogView::new(&*catalog, &*overlay);
418                dml::execute_delete(&mut sql_txn, &view, &table, filter)
419            }
420            LogicalPlan::Scan { .. }
421            | LogicalPlan::Filter { .. }
422            | LogicalPlan::Project { .. }
423            | LogicalPlan::Join { .. }
424            | LogicalPlan::Aggregate { .. }
425            | LogicalPlan::Window { .. }
426            | LogicalPlan::SetOperation { .. }
427            | LogicalPlan::RecursiveCte { .. }
428            | LogicalPlan::RecursiveReference { .. }
429            | LogicalPlan::Sort { .. }
430            | LogicalPlan::Limit { .. } => {
431                let view = TxnCatalogView::new(&*catalog, &*overlay);
432                query::execute_query(&mut sql_txn, &view, plan)
433            }
434        };
435
436        match result {
437            Ok(value) => {
438                sql_txn.flush_hnsw()?;
439                Ok(value)
440            }
441            Err(err) => {
442                let _ = sql_txn.abandon_hnsw();
443                Err(err)
444            }
445        }
446    }
447
448    fn map_catalog_error(err: CatalogError) -> ExecutorError {
449        match err {
450            CatalogError::Kv(e) => ExecutorError::Core(e),
451            CatalogError::Serialize(e) => ExecutorError::InvalidOperation {
452                operation: "CatalogPersistence".into(),
453                reason: e.to_string(),
454            },
455            CatalogError::InvalidKey(reason) => ExecutorError::InvalidOperation {
456                operation: "CatalogPersistence".into(),
457                reason,
458            },
459        }
460    }
461
462    fn execute_create_table_in_txn<'txn>(
463        &self,
464        catalog: &mut PersistentCatalog<S>,
465        txn: &mut impl crate::storage::SqlTxn<'txn, S>,
466        overlay: &mut CatalogOverlay,
467        mut table: crate::catalog::TableMetadata,
468        with_options: Vec<(String, String)>,
469        if_not_exists: bool,
470    ) -> Result<ExecutionResult>
471    where
472        S: 'txn,
473    {
474        if catalog.table_exists_in_txn(&table.name, overlay) {
475            return if if_not_exists {
476                Ok(ExecutionResult::Success)
477            } else {
478                Err(ExecutorError::TableAlreadyExists(table.name))
479            };
480        }
481
482        table.storage_options = ddl::create_table::parse_storage_options(&with_options)?;
483
484        let pk_index = if let Some(pk_columns) = table.primary_key.clone() {
485            let column_indices = pk_columns
486                .iter()
487                .map(|name| {
488                    table
489                        .get_column_index(name)
490                        .ok_or_else(|| ExecutorError::ColumnNotFound(name.clone()))
491                })
492                .collect::<Result<Vec<_>>>()?;
493            let index_id = catalog.next_index_id();
494            let index_name = ddl::create_pk_index_name(&table.name);
495            let mut index = crate::catalog::IndexMetadata::new(
496                index_id,
497                index_name,
498                table.name.clone(),
499                pk_columns,
500            )
501            .with_column_indices(column_indices)
502            .with_unique(true);
503            index.catalog_name = table.catalog_name.clone();
504            index.namespace_name = table.namespace_name.clone();
505            Some(index)
506        } else {
507            None
508        };
509
510        let table_id = catalog.next_table_id();
511        table = table.with_table_id(table_id);
512
513        // storage keyspace の初期化
514        txn.delete_prefix(&KeyEncoder::table_prefix(table_id))?;
515        txn.delete_prefix(&KeyEncoder::sequence_key(table_id))?;
516
517        // 永続化(同一 KV トランザクション内)
518        catalog
519            .persist_create_table(txn.inner_mut(), &table)
520            .map_err(Self::map_catalog_error)?;
521        if let Some(index) = &pk_index {
522            catalog
523                .persist_create_index(txn.inner_mut(), index)
524                .map_err(Self::map_catalog_error)?;
525        }
526
527        // オーバーレイに反映(ベースカタログはコミットまで不変)
528        overlay.add_table(TableFqn::from(&table), table);
529        if let Some(index) = pk_index {
530            overlay.add_index(IndexFqn::from(&index), index);
531        }
532
533        Ok(ExecutionResult::Success)
534    }
535
536    fn execute_drop_table_in_txn<'txn>(
537        &self,
538        catalog: &mut PersistentCatalog<S>,
539        txn: &mut impl crate::storage::SqlTxn<'txn, S>,
540        overlay: &mut CatalogOverlay,
541        table_name: &str,
542        if_exists: bool,
543    ) -> Result<ExecutionResult>
544    where
545        S: 'txn,
546    {
547        let table_meta = match catalog.get_table_in_txn(table_name, overlay) {
548            Some(table) => table.clone(),
549            None => {
550                return if if_exists {
551                    Ok(ExecutionResult::Success)
552                } else {
553                    Err(ExecutorError::TableNotFound(table_name.to_string()))
554                };
555            }
556        };
557        if table_meta.catalog_name != "default" || table_meta.namespace_name != "default" {
558            return if if_exists {
559                Ok(ExecutionResult::Success)
560            } else {
561                Err(ExecutorError::TableNotFound(table_name.to_string()))
562            };
563        }
564
565        let indexes = TxnCatalogView::new(catalog, overlay)
566            .get_indexes_for_table(table_name)
567            .into_iter()
568            .cloned()
569            .collect::<Vec<_>>();
570
571        for index in &indexes {
572            if matches!(index.method, Some(crate::ast::ddl::IndexMethod::Hnsw)) {
573                crate::executor::hnsw_bridge::HnswBridge::drop_index(txn, index, false)?;
574            } else {
575                txn.delete_prefix(&KeyEncoder::index_prefix(index.index_id))?;
576            }
577        }
578
579        txn.delete_prefix(&KeyEncoder::table_prefix(table_meta.table_id))?;
580        txn.delete_prefix(&KeyEncoder::sequence_key(table_meta.table_id))?;
581
582        catalog
583            .persist_drop_table(txn.inner_mut(), &TableFqn::from(&table_meta))
584            .map_err(Self::map_catalog_error)?;
585
586        overlay.drop_table(&TableFqn::from(&table_meta));
587
588        Ok(ExecutionResult::Success)
589    }
590
591    fn execute_create_index_in_txn<'txn>(
592        &self,
593        catalog: &mut PersistentCatalog<S>,
594        txn: &mut impl crate::storage::SqlTxn<'txn, S>,
595        overlay: &mut CatalogOverlay,
596        mut index: crate::catalog::IndexMetadata,
597        if_not_exists: bool,
598    ) -> Result<ExecutionResult>
599    where
600        S: 'txn,
601    {
602        if ddl::is_implicit_pk_index(&index.name) {
603            return Err(ExecutorError::InvalidIndexName {
604                name: index.name.clone(),
605                reason: "Index names starting with '__pk_' are reserved for PRIMARY KEY".into(),
606            });
607        }
608
609        if catalog.index_exists_in_txn(&index.name, overlay) {
610            return if if_not_exists {
611                Ok(ExecutionResult::Success)
612            } else {
613                Err(ExecutorError::IndexAlreadyExists(index.name))
614            };
615        }
616
617        let table = catalog
618            .get_table_in_txn(&index.table, overlay)
619            .ok_or_else(|| ExecutorError::TableNotFound(index.table.clone()))?
620            .clone();
621        index.catalog_name = table.catalog_name.clone();
622        index.namespace_name = table.namespace_name.clone();
623
624        let column_indices = index
625            .columns
626            .iter()
627            .map(|name| {
628                table
629                    .get_column_index(name)
630                    .ok_or_else(|| ExecutorError::ColumnNotFound(name.clone()))
631            })
632            .collect::<Result<Vec<_>>>()?;
633
634        let index_id = catalog.next_index_id();
635        index.index_id = index_id;
636        index.column_indices = column_indices.clone();
637
638        if matches!(index.method, Some(crate::ast::ddl::IndexMethod::Hnsw)) {
639            crate::executor::hnsw_bridge::HnswBridge::create_index(txn, &table, &index)?;
640        } else {
641            ddl::create_index::build_index_for_existing_rows(txn, &table, &index, column_indices)?;
642        }
643
644        catalog
645            .persist_create_index(txn.inner_mut(), &index)
646            .map_err(Self::map_catalog_error)?;
647
648        overlay.add_index(IndexFqn::from(&index), index);
649
650        Ok(ExecutionResult::Success)
651    }
652
653    fn execute_drop_index_in_txn<'txn>(
654        &self,
655        catalog: &mut PersistentCatalog<S>,
656        txn: &mut impl crate::storage::SqlTxn<'txn, S>,
657        overlay: &mut CatalogOverlay,
658        index_name: &str,
659        if_exists: bool,
660    ) -> Result<ExecutionResult>
661    where
662        S: 'txn,
663    {
664        if ddl::is_implicit_pk_index(index_name) {
665            return Err(ExecutorError::InvalidOperation {
666                operation: "DROP INDEX".into(),
667                reason: "Cannot drop implicit PRIMARY KEY index directly; use DROP TABLE".into(),
668            });
669        }
670
671        let index = match catalog.get_index_in_txn(index_name, overlay) {
672            Some(index) => index.clone(),
673            None => {
674                return if if_exists {
675                    Ok(ExecutionResult::Success)
676                } else {
677                    Err(ExecutorError::IndexNotFound(index_name.to_string()))
678                };
679            }
680        };
681        if index.catalog_name != "default" || index.namespace_name != "default" {
682            return if if_exists {
683                Ok(ExecutionResult::Success)
684            } else {
685                Err(ExecutorError::IndexNotFound(index_name.to_string()))
686            };
687        }
688
689        if matches!(index.method, Some(crate::ast::ddl::IndexMethod::Hnsw)) {
690            crate::executor::hnsw_bridge::HnswBridge::drop_index(txn, &index, if_exists)?;
691        } else {
692            txn.delete_prefix(&KeyEncoder::index_prefix(index.index_id))?;
693        }
694
695        catalog
696            .persist_drop_index(txn.inner_mut(), &IndexFqn::from(&index))
697            .map_err(Self::map_catalog_error)?;
698
699        overlay.drop_index(&IndexFqn::from(&index));
700
701        Ok(ExecutionResult::Success)
702    }
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708    use crate::catalog::MemoryCatalog;
709    use alopex_core::kv::memory::MemoryKV;
710
711    fn create_executor() -> Executor<MemoryKV, MemoryCatalog> {
712        let store = Arc::new(MemoryKV::new());
713        let catalog = Arc::new(RwLock::new(MemoryCatalog::new()));
714        Executor::new(store, catalog)
715    }
716
717    #[test]
718    fn test_executor_creation() {
719        let _executor = create_executor();
720        // Executor should be created without panic
721    }
722
723    #[test]
724    fn create_table_is_supported() {
725        let mut executor = create_executor();
726
727        use crate::catalog::{ColumnMetadata, TableMetadata};
728        use crate::planner::ResolvedType;
729
730        let table = TableMetadata::new(
731            "test",
732            vec![ColumnMetadata::new("id", ResolvedType::Integer)],
733        );
734
735        let result = executor.execute(LogicalPlan::CreateTable {
736            table,
737            if_not_exists: false,
738            with_options: vec![],
739        });
740        assert!(matches!(result, Ok(ExecutionResult::Success)));
741
742        let catalog = executor.catalog.read().unwrap();
743        assert!(catalog.table_exists("test"));
744    }
745
746    #[test]
747    fn insert_is_supported() {
748        use crate::Span;
749        use crate::catalog::{ColumnMetadata, TableMetadata};
750        use crate::planner::typed_expr::TypedExprKind;
751        use crate::planner::types::ResolvedType;
752
753        let mut executor = create_executor();
754
755        let table = TableMetadata::new("t", vec![ColumnMetadata::new("id", ResolvedType::Integer)])
756            .with_primary_key(vec!["id".into()]);
757
758        executor
759            .execute(LogicalPlan::CreateTable {
760                table,
761                if_not_exists: false,
762                with_options: vec![],
763            })
764            .unwrap();
765
766        let result = executor.execute(LogicalPlan::Insert {
767            table: "t".into(),
768            columns: vec!["id".into()],
769            values: vec![vec![crate::planner::typed_expr::TypedExpr {
770                kind: TypedExprKind::Literal(crate::ast::expr::Literal::Number("1".into())),
771                resolved_type: ResolvedType::Integer,
772                span: Span::default(),
773            }]],
774        });
775        assert!(matches!(result, Ok(ExecutionResult::RowsAffected(1))));
776    }
777
778    #[test]
779    fn system_pragma_and_stats_function_use_the_store() {
780        let mut executor = create_executor();
781        let catalog = MemoryCatalog::new();
782
783        let pragma = crate::Parser::parse_sql(&crate::AlopexDialect, "PRAGMA cache_size = 8")
784            .unwrap()
785            .pop()
786            .unwrap();
787        let plan = crate::Planner::new(&catalog).plan(&pragma).unwrap();
788        assert!(matches!(
789            executor.execute(plan),
790            Ok(ExecutionResult::Success)
791        ));
792
793        let select = crate::Parser::parse_sql(&crate::AlopexDialect, "SELECT memory_stats()")
794            .unwrap()
795            .pop()
796            .unwrap();
797        let plan = crate::Planner::new(&catalog).plan(&select).unwrap();
798        let result = executor.execute(plan).unwrap();
799        let ExecutionResult::Query(result) = result else {
800            panic!("expected query result");
801        };
802        assert_eq!(result.columns[0].name, "memory_stats");
803        assert!(
804            matches!(&result.rows[0][0], crate::SqlValue::Text(text) if text.contains("total_bytes"))
805        );
806    }
807}