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