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