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 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 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 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 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 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 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 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 txn.delete_prefix(&KeyEncoder::table_prefix(table_id))?;
527 txn.delete_prefix(&KeyEncoder::sequence_key(table_id))?;
528
529 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 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 }
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}