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;
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
69pub struct Executor<S: KVStore, C: Catalog> {
79 bridge: TxnBridge<S>,
81
82 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 pub fn new(store: Arc<S>, catalog: Arc<RwLock<C>>) -> Self {
111 Self {
112 bridge: TxnBridge::new(store),
113 catalog,
114 }
115 }
116
117 pub fn execute(&mut self, plan: LogicalPlan) -> Result<ExecutionResult> {
144 match plan {
145 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 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 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 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 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 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 txn.delete_prefix(&KeyEncoder::table_prefix(table_id))?;
446 txn.delete_prefix(&KeyEncoder::sequence_key(table_id))?;
447
448 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 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 }
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}