1use super::Connection;
2use super::plan_cache::{CachedPlan, normalize_query};
3use crate::database::Database;
4use crate::prepared_statement::PreparedStatement;
5use crate::query_result::QueryResult;
6use akar_binder::Binder;
7use akar_binder::bound_statement::BoundStatement;
8use akar_common::error::ProcessorError;
9use akar_common::types::Value;
10use akar_optimizer::Optimizer;
11use akar_parser::parse;
12use akar_planner::QueryPlanner;
13use akar_planner::logical_operator::LogicalOperator;
14use akar_processor::QueryProcessor;
15use akar_processor::processor::{SchemaDdlFn, SchemaDdlOp, SequenceFn, StandaloneCallHandler, SubqueryFn};
16use std::collections::HashMap;
17use std::sync::Arc;
18
19impl Connection {
20 pub fn query(&self, query_str: &str) -> Result<QueryResult, String> {
22 let trimmed = query_str.trim();
23
24 if trimmed.is_empty() {
26 return Ok(QueryResult::new(Vec::new()));
27 }
28
29 if let Some(value) = trimmed
31 .strip_prefix("SET")
32 .and_then(|s| s.trim().strip_prefix("spill_threshold"))
33 .and_then(|s| s.trim().strip_prefix("="))
34 .map(|s| s.trim())
35 {
36 let bytes: u64 = value.parse().map_err(|_| {
37 format!("Invalid spill_threshold value '{value}'. Expected a non-negative integer (bytes).")
38 })?;
39 self.database.set_spill_threshold(bytes);
40 return Ok(QueryResult::success_message(format!(
41 "spill_threshold set to {bytes} bytes"
42 )));
43 }
44
45 if let Some(value) = trimmed
46 .strip_prefix("SET")
47 .and_then(|s| s.trim().strip_prefix("concurrent_writes"))
48 .and_then(|s| s.trim().strip_prefix("="))
49 .map(|s| s.trim())
50 {
51 let enabled = match value.to_lowercase().as_str() {
52 "true" | "1" | "yes" => true,
53 "false" | "0" | "no" => false,
54 _ => return Err("Invalid value for concurrent_writes. Use true or false.".into()),
55 };
56 self.database.transaction_manager.set_concurrent_writes(enabled);
57 return Ok(QueryResult::success_message(format!(
58 "concurrent_writes set to {enabled}"
59 )));
60 }
61
62 let normalized = normalize_query(trimmed);
64
65 let catalog_version = self
68 .database
69 .catalog
70 .lock()
71 .map_err(|e| format!("Catalog lock error: {e}"))?
72 .version();
73
74 {
75 let mut cache = self.plan_cache.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
76 if let Some(cached) = cache.get(&normalized).filter(|c| c.catalog_version == catalog_version) {
77 let bound = cached.bound.clone();
80 let plan = cached.plan.clone();
81 drop(cache);
82 return self.execute_with_plan(&bound, Some(&plan));
83 }
84 }
85
86 let statement = parse(trimmed).map_err(|e| format!("Parse error: {e}"))?;
89
90 let binder = Binder::new(self.database.catalog.clone());
92 let bound = binder.bind(statement).map_err(|e| format!("Bind error: {e}"))?;
93
94 let plan_opt: Option<Arc<Vec<LogicalOperator>>> = if is_plan_cachable(&bound) {
98 let plan = Arc::new(self.build_optimized_plan(&bound)?);
99 let mut cache = self.plan_cache.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
100 cache.insert(
101 normalized,
102 CachedPlan {
103 bound: Arc::new(bound.clone()),
104 plan: Arc::clone(&plan),
105 catalog_version,
106 },
107 );
108 Some(plan)
109 } else {
110 None
111 };
112
113 self.execute_with_plan(&bound, plan_opt.as_ref())
114 }
115
116 fn execute_with_plan(
120 &self,
121 bound: &BoundStatement,
122 plan: Option<&Arc<Vec<LogicalOperator>>>,
123 ) -> Result<QueryResult, String> {
124 if self.database.config.read_only && Connection::is_write_statement(bound) {
126 return Err("Database is in read-only mode; write statements are not allowed".into());
127 }
128
129 let is_write = Connection::is_write_statement(bound);
134 let mut txn_opt: Option<akar_transaction::Transaction> =
135 if is_write { Some(self.begin_write_txn()?) } else { None };
136
137 let query_result = self.execute_query_inner(bound, txn_opt.as_mut(), plan);
139
140 match (is_write, &query_result) {
142 (true, Ok(_)) => {
143 if let Some(ref mut txn) = txn_opt {
144 self.commit_write_txn(txn)?;
145 }
146 }
147 (true, Err(e)) => {
148 if let Some(ref mut txn) = txn_opt {
149 match self.rollback_write_txn(txn) {
150 Ok(_records) => {
151 tracing::warn!("Transaction rolled back due to error: {e}");
152 }
153 Err(rollback_err) => {
154 tracing::error!("Transaction rollback ALSO failed: {rollback_err} (original error: {e})");
155 }
156 }
157 }
158 }
159 _ => {}
160 }
161
162 if query_result.is_ok() && Connection::is_write_statement(bound) {
165 let written = Connection::extract_write_tables(bound);
166 if !written.is_empty() {
167 self.database.refresh_vector_indexes(&written);
168 }
169 }
170
171 query_result
172 }
173
174 fn build_optimized_plan(&self, bound: &BoundStatement) -> Result<Vec<LogicalOperator>, String> {
176 let planner = QueryPlanner::new();
177 let logical_plan = planner.plan(bound.clone()).map_err(|e| format!("Plan error: {e}"))?;
178 let optimizer = Optimizer::with_stats(self.database.stats_store.clone());
179 Ok(optimizer.optimize(logical_plan))
180 }
181
182 pub(crate) fn execute_query_inner(
188 &self,
189 bound: &BoundStatement,
190 mut txn_opt: Option<&mut akar_transaction::Transaction>,
191 cached_plan: Option<&Arc<Vec<LogicalOperator>>>,
192 ) -> Result<QueryResult, String> {
193 if let Some(result) = self.handle_ddl(bound, txn_opt.as_deref_mut())? {
195 self.database.persist_catalog()?;
198 self.maybe_auto_checkpoint()?;
199 return Ok(result);
200 }
201
202 if let Some(ref txn) = txn_opt {
206 if !self.database.transaction_manager.allow_concurrent_writes() {
207 let write_tables = Connection::extract_write_tables(bound);
208 for tid in write_tables {
209 self.database.transaction_manager.lock_table(txn.transaction_id, tid)?;
210 }
211 }
212 }
213
214 let optimized_plan: Arc<Vec<LogicalOperator>> = match cached_plan {
218 Some(plan) => plan.clone(),
219 None => Arc::new(self.build_optimized_plan(bound)?),
220 };
221
222 if optimized_plan.is_empty() {
223 return Ok(QueryResult::success_message("Query executed (no result)".into()));
224 }
225
226 let (snapshot_ts, commit_history) = if let Some(ref txn) = txn_opt {
230 (
231 txn.snapshot_ts,
232 self.database.transaction_manager.commit_history_snapshot(),
233 )
234 } else {
235 let ts = self.database.transaction_manager.current_commit_ts();
237 let history = self.database.transaction_manager.commit_history_snapshot();
238 (Some(ts), history)
239 };
240
241 let processor = self
243 .create_processor()
244 .with_snapshot(snapshot_ts, commit_history)
245 .with_txn_id(txn_opt.as_ref().map(|t| t.transaction_id));
246 let chunks = processor
247 .execute(&optimized_plan)
248 .map_err(|e| format!("Execute error: {e}"))?;
249
250 if let Some(ref txn) = txn_opt {
252 let written_rows = processor.take_written_rows();
253 let tm = &self.database.transaction_manager;
254 for (table_id, row_id) in written_rows {
255 tm.record_write(txn.transaction_id, table_id, row_id);
256 }
257 }
258
259 if let Some(ref mut txn) = txn_opt {
262 let undo = processor.take_undo_records();
263 txn.undo_records.extend(undo);
264 let wal_records = processor.take_wal_records();
266 self.append_local_wal(txn.transaction_id, wal_records);
267 }
268
269 self.maybe_auto_checkpoint()?;
271
272 Ok(QueryResult::new(chunks))
273 }
274
275 pub fn prepare(&self, query_str: &str) -> Result<PreparedStatement, String> {
281 let trimmed = query_str.trim();
282
283 {
285 let cache = self.statement_cache.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
286 if let Some(cached) = cache.get(trimmed) {
287 return Ok(cached.clone());
288 }
289 }
290
291 let statement = parse(trimmed).map_err(|e| format!("Parse error: {e}"))?;
293
294 let binder = Binder::new(self.database.catalog.clone());
296 let bound = binder.bind(statement).map_err(|e| format!("Bind error: {e}"))?;
297
298 let prepared = PreparedStatement::new(trimmed.to_string(), bound);
299
300 {
302 let mut cache = self.statement_cache.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
303 cache.insert(trimmed.to_string(), prepared.clone());
304 }
305
306 Ok(prepared)
307 }
308
309 pub fn execute(&self, prepared: &PreparedStatement, params: Vec<(&str, Value)>) -> Result<QueryResult, String> {
315 let mut param_map = HashMap::new();
317 let num_expected = prepared.parameters.len();
318 for (name, value) in ¶ms {
319 param_map.insert(name.to_string(), value.clone());
320 }
321
322 for p in &prepared.parameters {
324 if !param_map.contains_key(p) {
325 return Err(format!("Missing parameter: ${}", p));
326 }
327 }
328
329 if params.len() > num_expected {
331 return Err(format!("Expected {} parameter(s), got {}", num_expected, params.len()));
332 }
333
334 if self.database.config.read_only && Connection::is_write_statement(&prepared.bound_statement) {
336 return Err("Database is in read-only mode; write statements are not allowed".into());
337 }
338
339 let substituted =
343 crate::connection::substitute::substitute_params_in_statement(&prepared.bound_statement, ¶m_map)?;
344
345 let is_write = Connection::is_write_statement(&prepared.bound_statement);
353 let mut txn_opt: Option<akar_transaction::Transaction> =
354 if is_write { Some(self.begin_write_txn()?) } else { None };
355
356 match self.handle_ddl(&substituted, txn_opt.as_mut()) {
359 Ok(Some(result)) => {
360 self.database.persist_catalog()?;
361 self.maybe_auto_checkpoint()?;
362 if is_write {
363 if let Some(ref mut txn) = txn_opt {
364 self.commit_write_txn(txn)?;
365 }
366 }
367 if is_write {
368 let written = Connection::extract_write_tables(&substituted);
369 if !written.is_empty() {
370 self.database.refresh_vector_indexes(&written);
371 }
372 }
373 return Ok(result);
374 }
375 Ok(None) => {}
376 Err(e) => {
377 if is_write {
378 if let Some(ref mut txn) = txn_opt {
379 match self.rollback_write_txn(txn) {
380 Ok(_) => tracing::warn!("Prepared DDL/DML rolled back due to error: {e}"),
381 Err(rollback_err) => {
382 tracing::error!("Prepared rollback ALSO failed: {rollback_err} (original: {e})");
383 }
384 }
385 }
386 }
387 return Err(e);
388 }
389 }
390
391 let planner = QueryPlanner::new();
393 let logical_plan = match planner.plan(substituted) {
394 Ok(p) => p,
395 Err(e) => {
396 if is_write {
397 if let Some(ref mut txn) = txn_opt {
398 let _ = self.rollback_write_txn(txn);
399 }
400 }
401 return Err(format!("Plan error: {e}"));
402 }
403 };
404
405 if logical_plan.is_empty() {
406 if is_write {
407 if let Some(ref mut txn) = txn_opt {
408 self.commit_write_txn(txn)?;
409 }
410 }
411 return Ok(QueryResult::success_message("Query executed (no result)".into()));
412 }
413
414 let optimizer = Optimizer::with_stats(self.database.stats_store.clone());
416 let optimized_plan = optimizer.optimize(logical_plan);
417
418 let (snapshot_ts, history) = if let Some(ref txn) = txn_opt {
420 (
421 txn.snapshot_ts,
422 self.database.transaction_manager.commit_history_snapshot(),
423 )
424 } else {
425 let ts = self.database.transaction_manager.current_commit_ts();
426 (Some(ts), self.database.transaction_manager.commit_history_snapshot())
427 };
428
429 let processor = self
431 .create_processor()
432 .with_snapshot(snapshot_ts, history)
433 .with_txn_id(txn_opt.as_ref().map(|t| t.transaction_id));
434 let chunks = match processor.execute(&optimized_plan) {
435 Ok(c) => c,
436 Err(e) => {
437 if is_write {
438 if let Some(ref mut txn) = txn_opt {
439 match self.rollback_write_txn(txn) {
440 Ok(_) => tracing::warn!("Prepared write rolled back due to error: {e}"),
441 Err(rollback_err) => {
442 tracing::error!("Prepared rollback ALSO failed: {rollback_err} (original: {e})");
443 }
444 }
445 }
446 }
447 return Err(format!("Execute error: {e}"));
448 }
449 };
450
451 if let Some(ref txn) = txn_opt {
453 let written_rows = processor.take_written_rows();
454 let tm = &self.database.transaction_manager;
455 for (table_id, row_id) in written_rows {
456 tm.record_write(txn.transaction_id, table_id, row_id);
457 }
458 }
459
460 if let Some(ref mut txn) = txn_opt {
462 let undo = processor.take_undo_records();
463 txn.undo_records.extend(undo);
464 let wal_records = processor.take_wal_records();
466 self.append_local_wal(txn.transaction_id, wal_records);
467 }
468
469 if is_write {
473 if let Some(ref mut txn) = txn_opt {
474 self.commit_write_txn(txn)?;
475 }
476 }
477
478 if is_write {
482 let written = Connection::extract_write_tables(&prepared.bound_statement);
483 if !written.is_empty() {
484 self.database.refresh_vector_indexes(&written);
485 }
486 }
487
488 self.maybe_auto_checkpoint()?;
490
491 Ok(QueryResult::new(chunks))
492 }
493
494 pub(crate) fn maybe_auto_checkpoint(&self) -> Result<(), String> {
499 if !self.database.config.auto_checkpoint {
500 return Ok(()); }
502 let threshold = self.database.config.checkpoint_threshold;
503 if threshold == 0 {
504 return Ok(()); }
506
507 let should_checkpoint = if threshold < 0 {
508 true
509 } else {
510 self.database.storage_manager.wal_size() > threshold as usize
511 };
512
513 if should_checkpoint {
514 self.database.transaction_manager.schedule_auto_checkpoint();
516 tracing::debug!("Auto-checkpoint signaled to background worker");
517 }
518
519 Ok(())
520 }
521
522 pub(crate) fn do_sync_checkpoint(&self) -> Result<(), String> {
524 let tm = &self.database.transaction_manager;
525 let drain_fn = |timeout: std::time::Duration| -> bool { tm.stop_new_txns_and_wait_until_all_leave(timeout) };
526 self.database
527 .storage_manager
528 .checkpoint_with_drain(Some(&drain_fn))
529 .map_err(|e| format!("Checkpoint failed: {e}"))?;
530 tracing::debug!("Sync checkpoint completed");
531 Ok(())
532 }
533
534 pub(crate) fn create_processor(&self) -> QueryProcessor {
541 let handlers = self
542 .processor_handlers
543 .get_or_init(|| Arc::new(build_processor_handlers(&self.database)));
544
545 QueryProcessor::with_catalog(
546 self.database.function_registry.clone(),
547 self.database.table_catalog(),
548 self.database.vfs.clone(),
549 )
550 .with_sequence_fn(handlers.sequence_fn.clone())
551 .with_subquery_fn(handlers.subquery_fn.clone())
552 .with_schema_ddl_fn(handlers.schema_ddl_fn.clone())
553 .with_standalone_call_handler(handlers.standalone_call_handler.clone())
554 }
555}
556
557pub(crate) struct ProcessorHandlers {
562 pub sequence_fn: SequenceFn,
563 pub schema_ddl_fn: SchemaDdlFn,
564 pub subquery_fn: SubqueryFn,
565 pub standalone_call_handler: Arc<dyn StandaloneCallHandler>,
566}
567
568fn build_processor_handlers(db: &Arc<Database>) -> ProcessorHandlers {
570 let seq_fn = super::utils::make_sequence_callback(db.catalog.clone());
571
572 let db_sddl = db.clone();
574 let schema_ddl_fn: SchemaDdlFn = Arc::new(move |op: SchemaDdlOp| -> Result<String, ProcessorError> {
575 match op {
576 SchemaDdlOp::CreateSequence {
577 name,
578 if_not_exists,
579 start_value,
580 increment,
581 min_value,
582 max_value,
583 cycle,
584 } => {
585 let mut catalog = db_sddl.catalog.lock().map_err(|e| format!("Catalog lock: {e}"))?;
586 match catalog.create_sequence(name.clone(), start_value, increment, min_value, max_value, cycle) {
587 akar_catalog::CatalogResult::Created { .. } => Ok(format!("Sequence '{}' created", name)),
588 akar_catalog::CatalogResult::AlreadyExists => {
589 if if_not_exists {
590 Ok(format!("Sequence '{}' already exists", name))
591 } else {
592 Err(ProcessorError::Execution(format!("Sequence '{}' already exists", name)))
593 }
594 }
595 other => Err(ProcessorError::Execution(format!(
596 "Failed to create sequence: {:?}",
597 other
598 ))),
599 }
600 }
601 SchemaDdlOp::DropSequence { name, if_exists } => {
602 let mut catalog = db_sddl.catalog.lock().map_err(|e| format!("Catalog lock: {e}"))?;
603 match catalog.drop_sequence(&name) {
604 akar_catalog::CatalogResult::Dropped { .. } => Ok(format!("Sequence '{}' dropped", name)),
605 akar_catalog::CatalogResult::NotFound => {
606 if if_exists {
607 Ok(format!("Sequence '{}' not found", name))
608 } else {
609 Err(ProcessorError::Execution(format!("Sequence '{}' not found", name)))
610 }
611 }
612 other => Err(ProcessorError::Execution(format!(
613 "Failed to drop sequence: {:?}",
614 other
615 ))),
616 }
617 }
618 SchemaDdlOp::ExportDatabase {
619 file_path,
620 file_type,
621 schema_only,
622 } => {
623 let conn = super::Connection::new(&db_sddl);
628 let bound = akar_binder::bound_statement::BoundExportDatabase {
629 file_path,
630 file_type,
631 schema_only,
632 options: Default::default(),
633 };
634 let result = conn
635 .execute_export_database(&bound)
636 .map_err(ProcessorError::Execution)?;
637 let msg = result
638 .and_then(|r| r.message)
639 .unwrap_or_else(|| format!("Database exported to '{}'", bound.file_path));
640 Ok(msg)
641 }
642 SchemaDdlOp::ImportDatabase {
643 file_path,
644 query,
645 index_query,
646 } => {
647 let conn = super::Connection::new(&db_sddl);
651 let mut executed = 0usize;
652 let mut skipped = 0usize;
653 for stmt in super::copy::split_cypher_statements(&query)
654 .into_iter()
655 .chain(super::copy::split_cypher_statements(&index_query))
656 {
657 match conn.query(&stmt) {
658 Ok(_) => executed += 1,
659 Err(e) => {
660 tracing::warn!("Import statement skipped (may be duplicate): {e}");
661 skipped += 1;
662 }
663 }
664 }
665 Ok(format!(
666 "Imported {executed} statement(s) from '{file_path}' ({skipped} skipped)"
667 ))
668 }
669 }
670 });
671
672 let db_qf = db.clone();
674 let query_fn: crate::connection::standalone_call::QueryFn = Arc::new({
675 let schema_ddl_qf = schema_ddl_fn.clone();
676 move |query_str: &str| -> Result<crate::query_result::QueryResult, String> {
677 let stmt = akar_parser::parse(query_str).map_err(|e| format!("Parse error: {e}"))?;
678 let binder = Binder::new(db_qf.catalog.clone());
679 let bound = binder.bind(stmt).map_err(|e| format!("Bind error: {e}"))?;
680 let planner = QueryPlanner::new();
681 let logical_plan = planner.plan(bound).map_err(|e| format!("Plan error: {e}"))?;
682 let optimizer = Optimizer::with_stats(db_qf.stats_store.clone());
683 let optimized_plan = optimizer.optimize(logical_plan);
684
685 let processor = QueryProcessor::with_catalog(
686 db_qf.function_registry.clone(),
687 db_qf.table_catalog(),
688 db_qf.vfs.clone(),
689 )
690 .with_schema_ddl_fn(schema_ddl_qf.clone())
691 .with_standalone_call_handler(Arc::new(
692 crate::connection::standalone_call::DbStandaloneCallHandler::new(db_qf.clone()),
693 ))
694 .with_snapshot(
695 Some(db_qf.transaction_manager.current_commit_ts()),
696 db_qf.transaction_manager.commit_history_snapshot(),
697 );
698
699 let chunks = processor
700 .execute(&optimized_plan)
701 .map_err(|e| format!("Execute error: {e}"))?;
702
703 let num_rows: usize = chunks.iter().map(|c| c.size).sum();
704 let num_columns = chunks.first().map(|c| c.num_fields()).unwrap_or(0);
705 Ok(crate::query_result::QueryResult {
706 chunks,
707 num_rows,
708 num_columns,
709 success: true,
710 error_message: None,
711 message: None,
712 summary: None,
713 })
714 }
715 });
716
717 let db_sq = db.clone();
718 let subquery_fn: SubqueryFn = Arc::new({
719 let schema_ddl_sq = schema_ddl_fn.clone();
720 move |query: &akar_parser::ast::Query| -> Result<Vec<akar_common::vector::DataChunk>, ProcessorError> {
721 let stmt = akar_parser::ast::Statement::Query(query.clone());
722 let binder = Binder::new(db_sq.catalog.clone());
723 let bound = binder.bind(stmt).map_err(|e| format!("Bind error: {e}"))?;
724 let planner = QueryPlanner::new();
725 let logical_plan = planner.plan(bound).map_err(|e| format!("Plan error: {e}"))?;
726 let optimizer = Optimizer::with_stats(db_sq.stats_store.clone());
727 let optimized_plan = optimizer.optimize(logical_plan);
728
729 let catalog_inner = db_sq.catalog.clone();
730 let seq_fn_inner = super::utils::make_sequence_callback(catalog_inner);
731
732 let processor = QueryProcessor::with_catalog(
733 db_sq.function_registry.clone(),
734 db_sq.table_catalog(),
735 db_sq.vfs.clone(),
736 )
737 .with_sequence_fn(seq_fn_inner)
738 .with_schema_ddl_fn(schema_ddl_sq.clone())
739 .with_standalone_call_handler(Arc::new(
740 crate::connection::standalone_call::DbStandaloneCallHandler::new(db_sq.clone()),
741 ))
742 .with_snapshot(
743 Some(db_sq.transaction_manager.current_commit_ts()),
744 db_sq.transaction_manager.commit_history_snapshot(),
745 );
746
747 processor
748 .execute(&optimized_plan)
749 .map_err(|e| ProcessorError::Execution(format!("Execute error: {e}")))
750 }
751 });
752
753 let standalone_call_handler: Arc<dyn StandaloneCallHandler> = Arc::new(
754 crate::connection::standalone_call::DbStandaloneCallHandler::with_query_executor(
755 db.clone(),
756 Some(query_fn.clone()),
757 ),
758 );
759
760 ProcessorHandlers {
761 sequence_fn: seq_fn,
762 schema_ddl_fn,
763 subquery_fn,
764 standalone_call_handler,
765 }
766}
767
768fn is_plan_cachable(bound: &BoundStatement) -> bool {
776 match bound {
777 BoundStatement::BoundQuery(q) => {
778 !(q.clauses.len() == 1
779 && matches!(
780 q.clauses.first(),
781 Some(akar_binder::bound_statement::BoundClause::BoundForeach(_))
782 ))
783 }
784 _ => false,
785 }
786}