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