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