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