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