1use super::{
13 executor::{QueryExecutor, QueryResult},
14 parser::QueryParser,
15 planner::QueryPlanner,
16 prepared::PreparedQuery,
17 result::{QueryResultIterator, StreamingConfig},
18 QueryStats,
19};
20
21use super::engine_stats::AtomicQueryStats;
22#[cfg(feature = "state_machine")]
23use super::{
24 select_executor::SelectExecutor,
25 select_optimizer::{OptimizedQueryPlan, SelectOptimizer},
26 select_parser,
27};
28use crate::{
29 memory::MemoryManager, schema::SchemaManager, storage::StorageEngine, Config, Error, Result,
30 Value,
31};
32use dashmap::DashMap;
33use std::sync::atomic::{AtomicU64, Ordering};
34use std::sync::Arc;
35use std::time::Instant;
36
37#[derive(Debug)]
43pub(crate) struct QueryCacheEntry {
44 pub parsed_query: super::ParsedQuery,
46 pub plan: super::planner::QueryPlan,
48 pub cached_at: Instant,
50 pub hit_count: AtomicU64,
54}
55
56impl Clone for QueryCacheEntry {
60 fn clone(&self) -> Self {
61 Self {
62 parsed_query: self.parsed_query.clone(),
63 plan: self.plan.clone(),
64 cached_at: self.cached_at,
65 hit_count: AtomicU64::new(self.hit_count.load(Ordering::Relaxed)),
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72pub enum SchemaStatus {
73 Available { keyspace: String, table: String },
75 Missing { table: String, reason: String },
77 ExtractionFailed {
79 table: String,
80 cause: String,
81 suggestion: String,
82 },
83}
84
85#[derive(Debug)]
87pub struct QueryEngine {
88 parser: QueryParser,
90 planner: QueryPlanner,
92 executor: QueryExecutor,
94 schema_manager: Arc<SchemaManager>,
96 #[cfg(feature = "state_machine")]
99 select_optimizer: Arc<SelectOptimizer>,
100 #[cfg(feature = "state_machine")]
102 select_executor: Arc<SelectExecutor>,
103 prepared_cache: DashMap<String, Arc<PreparedQuery>>,
105 plan_cache: DashMap<String, QueryCacheEntry>,
107 #[cfg(feature = "state_machine")]
114 select_plan_cache: DashMap<String, (Instant, Arc<OptimizedQueryPlan>)>,
115 stats: AtomicQueryStats,
118 config: Config,
120}
121
122impl QueryEngine {
123 pub fn new(
125 storage: Arc<StorageEngine>,
126 schema: Arc<SchemaManager>,
127 _memory: Arc<MemoryManager>,
128 config: &Config,
129 ) -> Result<Self> {
130 let parser = QueryParser::new(config);
131 let planner = QueryPlanner::new(schema.clone(), config);
132 let executor = QueryExecutor::new(storage.clone(), schema.clone(), config);
133
134 #[cfg(feature = "state_machine")]
136 let select_optimizer = Arc::new(SelectOptimizer::new(schema.clone(), storage.clone()));
137 #[cfg(feature = "state_machine")]
140 let select_executor = Arc::new(
141 SelectExecutor::new(schema.clone(), storage)
142 .with_max_result_bytes(
143 usize::try_from(config.query.max_result_bytes).unwrap_or(usize::MAX),
144 )
145 .with_max_result_rows(
146 usize::try_from(config.query.max_result_rows).unwrap_or(usize::MAX),
147 )
148 .with_forced_read_path(config.query.forced_read_path),
152 );
153
154 Ok(Self {
155 parser,
156 planner,
157 executor,
158 schema_manager: schema,
159 #[cfg(feature = "state_machine")]
160 select_optimizer,
161 #[cfg(feature = "state_machine")]
162 select_executor,
163 prepared_cache: DashMap::new(),
164 plan_cache: DashMap::new(),
165 #[cfg(feature = "state_machine")]
166 select_plan_cache: DashMap::new(),
167 stats: AtomicQueryStats::default(),
168 config: config.clone(),
169 })
170 }
171
172 fn enforce_legacy_result_budget(&self, result: &QueryResult) -> Result<()> {
180 super::result_budget::enforce_materialized_rows(
181 &result.rows,
182 usize::try_from(self.config.query.max_result_bytes).unwrap_or(usize::MAX),
183 usize::try_from(self.config.query.max_result_rows).unwrap_or(usize::MAX),
184 )
185 .inspect_err(|e| {
186 self.inc_error_queries();
187 crate::observability::record_error(e, "query");
188 })
189 }
190
191 fn inc_total_queries(&self) {
193 self.stats.record_query();
194 }
195
196 fn inc_error_queries(&self) {
198 self.stats.record_error();
199 }
200
201 fn record_cache_hit(&self) {
204 self.stats.record_cache_hit();
205 }
206
207 #[tracing::instrument(
216 name = "query.execute",
217 skip(self, cql),
218 fields(
219 cqlite.query.plan_type = tracing::field::Empty,
220 cqlite.query.access_path = tracing::field::Empty,
221 cqlite.query.rows = tracing::field::Empty,
222 )
223 )]
224 pub async fn execute(&self, cql: &str) -> Result<QueryResult> {
225 let start_time = Instant::now();
226 self.inc_total_queries();
227
228 let trimmed_cql = cql.trim().to_uppercase();
238 if trimmed_cql.starts_with("SELECT") {
239 return self.execute_select_query(cql, start_time).await;
240 }
241
242 let cached_plan = self.plan_cache.get(cql).map(|entry| {
248 entry.hit_count.fetch_add(1, Ordering::Relaxed);
249 entry.plan.clone()
250 });
251 if let Some(plan) = cached_plan {
252 self.record_cache_hit();
253
254 let mut result =
255 crate::observability::record_result("query", self.executor.execute(&plan).await)?;
256 self.enforce_legacy_result_budget(&result)?;
261 self.update_execution_stats(&mut result, start_time);
262 return Ok(result);
263 }
264
265 let parsed_query = self.parser.parse(cql).inspect_err(|e| {
266 self.inc_error_queries();
267 crate::observability::record_error(e, "query");
268 })?;
269 let plan =
270 crate::observability::record_result("query", self.planner.plan(&parsed_query).await)?;
271
272 if self.config.query.query_cache_size.unwrap_or(0) > 0 {
273 self.cache_query_plan(cql, parsed_query, plan.clone());
274 }
275
276 let mut result =
277 crate::observability::record_result("query", self.executor.execute(&plan).await)?;
278 self.enforce_legacy_result_budget(&result)?;
282 self.update_execution_stats(&mut result, start_time);
283 Ok(result)
284 }
285
286 #[cfg(feature = "state_machine")]
308 pub async fn execute_streaming(
309 &self,
310 cql: &str,
311 config: StreamingConfig,
312 ) -> Result<QueryResultIterator> {
313 self.inc_total_queries();
314
315 if !cql.trim().to_uppercase().starts_with("SELECT") {
316 return Err(Error::query_execution(
317 "Streaming execution only supports SELECT queries",
318 ));
319 }
320
321 let select_statement =
322 select_parser::parse_select(cql).inspect_err(|_| self.inc_error_queries())?;
323 let optimized_plan = self.select_optimizer.optimize(select_statement).await?;
324
325 self.select_executor
326 .execute_streaming(optimized_plan, config)
327 .await
328 }
329
330 async fn execute_select_query(&self, cql: &str, start_time: Instant) -> Result<QueryResult> {
332 enum HitOutcome {
338 Reuse(super::planner::QueryPlan),
339 Placeholder,
340 Miss,
341 }
342 let outcome = match self.plan_cache.get(cql) {
343 Some(entry) if entry.plan.table.is_some() => {
344 entry.hit_count.fetch_add(1, Ordering::Relaxed);
345 HitOutcome::Reuse(entry.plan.clone())
346 }
347 Some(_) => HitOutcome::Placeholder,
348 None => HitOutcome::Miss,
349 };
350 match outcome {
351 HitOutcome::Reuse(plan) => {
352 self.record_cache_hit();
353
354 let mut result = crate::observability::record_result(
355 "query",
356 self.executor.execute(&plan).await,
357 )?;
358 self.enforce_legacy_result_budget(&result)?;
362 self.update_execution_stats(&mut result, start_time);
363 return Ok(result);
364 }
365 HitOutcome::Placeholder => {
366 self.plan_cache.remove(cql);
368 }
369 HitOutcome::Miss => {}
370 }
371
372 #[cfg(not(feature = "state_machine"))]
373 return Err(Error::query_execution(
374 "Advanced SELECT parsing requires state_machine feature",
375 ));
376
377 #[cfg(feature = "state_machine")]
378 {
379 let optimized_plan: Arc<OptimizedQueryPlan> =
384 if let Some(entry) = self.select_plan_cache.get(cql) {
385 self.record_cache_hit();
386 Arc::clone(&entry.value().1)
387 } else {
388 let select_statement = select_parser::parse_select(cql).inspect_err(|e| {
389 self.inc_error_queries();
390 crate::observability::record_error(e, "query");
391 })?;
392 let plan = Arc::new(crate::observability::record_result(
393 "query",
394 self.select_optimizer.optimize(select_statement).await,
395 )?);
396 self.cache_select_plan(cql, Arc::clone(&plan));
397 plan
398 };
399 let mut result = crate::observability::record_result(
400 "query",
401 self.select_executor
402 .execute((*optimized_plan).clone())
403 .await,
404 )?;
405 self.update_execution_stats(&mut result, start_time);
406 Ok(result)
407 }
408 }
409
410 #[cfg(feature = "state_machine")]
414 fn cache_select_plan(&self, cql: &str, plan: Arc<OptimizedQueryPlan>) {
415 let cache_size = self.config.query.query_cache_size.unwrap_or(0);
416 if cache_size == 0 {
417 return;
418 }
419 if self.select_plan_cache.len() >= cache_size {
420 let oldest_key = self
421 .select_plan_cache
422 .iter()
423 .min_by_key(|entry| entry.value().0)
424 .map(|entry| entry.key().clone());
425 if let Some(key) = oldest_key {
426 self.select_plan_cache.remove(&key);
427 }
428 }
429 self.select_plan_cache
430 .insert(cql.to_string(), (Instant::now(), plan));
431 }
432
433 pub async fn execute_with_params(&self, cql: &str, params: &[Value]) -> Result<QueryResult> {
461 let is_select = cql.trim().to_uppercase().starts_with("SELECT");
462
463 if !is_select {
464 if params.is_empty() {
468 return self.execute(cql).await;
469 }
470 self.inc_total_queries();
471 self.inc_error_queries();
472 return Err(Error::query_execution(
473 "Parameterized execution currently supports SELECT statements only",
474 ));
475 }
476
477 #[cfg(not(feature = "state_machine"))]
478 {
479 let _ = params;
480 self.inc_total_queries();
481 self.inc_error_queries();
482 return Err(Error::query_execution(
483 "Parameterized SELECT execution requires the state_machine feature",
484 ));
485 }
486
487 #[cfg(feature = "state_machine")]
488 {
489 let statement = select_parser::parse_select(cql).inspect_err(|_| {
492 self.inc_total_queries();
493 self.inc_error_queries();
494 })?;
495 let marker_count = statement.bind_marker_count();
496
497 if marker_count == 0 {
502 if params.is_empty() {
503 return self.execute(cql).await;
504 }
505 self.inc_total_queries();
506 self.inc_error_queries();
507 return Err(Error::query_execution(format!(
508 "Parameter count mismatch: query has 0 bind marker(s), got {} parameter(s)",
509 params.len()
510 )));
511 }
512
513 let start_time = Instant::now();
514 self.inc_total_queries();
515
516 let mut statement = statement;
521 statement
522 .bind_parameters(params)
523 .inspect_err(|_| self.inc_error_queries())?;
524
525 let optimized_plan = self.select_optimizer.optimize(statement).await?;
526 let mut result = self.select_executor.execute(optimized_plan).await?;
527 self.update_execution_stats(&mut result, start_time);
528 Ok(result)
529 }
530 }
531
532 pub async fn prepare(&self, cql: &str) -> Result<Arc<PreparedQuery>> {
534 if let Some(cached) = self.prepared_cache.get(cql) {
535 return Ok(cached.clone());
536 }
537
538 let parsed_query = self.parser.parse(cql)?;
539 let plan = self.planner.plan(&parsed_query).await?;
540
541 #[cfg(feature = "state_machine")]
547 let prepared = if cql.trim().to_uppercase().starts_with("SELECT") {
548 let statement = select_parser::parse_select(cql)?;
549 let marker_count = statement.bind_marker_count();
550 Arc::new(PreparedQuery::new_select(
551 parsed_query,
552 plan,
553 Arc::new(self.executor.clone()),
554 statement,
555 marker_count,
556 self.select_optimizer.clone(),
557 self.select_executor.clone(),
558 ))
559 } else {
560 Arc::new(PreparedQuery::new(
561 parsed_query,
562 plan,
563 Arc::new(self.executor.clone()),
564 ))
565 };
566
567 #[cfg(not(feature = "state_machine"))]
568 let prepared = Arc::new(PreparedQuery::new(
569 parsed_query,
570 plan,
571 Arc::new(self.executor.clone()),
572 ));
573
574 self.prepared_cache
575 .insert(cql.to_string(), prepared.clone());
576
577 Ok(prepared)
578 }
579
580 pub async fn execute_prepared(
582 &self,
583 prepared: &PreparedQuery,
584 params: &[Value],
585 ) -> Result<QueryResult> {
586 let start_time = Instant::now();
587 self.inc_total_queries();
588
589 let mut result = prepared.execute(params).await?;
590 self.update_execution_stats(&mut result, start_time);
591 Ok(result)
592 }
593
594 pub fn stats(&self) -> QueryStats {
596 self.stats.snapshot()
597 }
598
599 pub fn clear_caches(&self) {
601 self.prepared_cache.clear();
602 self.plan_cache.clear();
603 #[cfg(feature = "state_machine")]
604 self.select_plan_cache.clear();
605 }
606
607 pub fn clear_prepared_cache(&self) {
609 self.prepared_cache.clear();
610 }
611
612 pub fn clear_plan_cache(&self) {
614 self.plan_cache.clear();
615 #[cfg(feature = "state_machine")]
616 self.select_plan_cache.clear();
617 }
618
619 pub fn cache_stats(&self) -> CacheStats {
621 CacheStats {
622 prepared_cache_size: self.prepared_cache.len(),
623 plan_cache_size: self.plan_cache.len(),
624 prepared_cache_hits: self.prepared_cache.len() as u64,
625 plan_cache_hits: self.plan_cache.len() as u64,
626 }
627 }
628
629 pub async fn explain(&self, cql: &str) -> Result<ExplainResult> {
631 let parsed_query = self.parser.parse(cql)?;
633
634 let plan = self.planner.plan(&parsed_query).await?;
636
637 Ok(ExplainResult {
638 query_type: format!("{:?}", parsed_query.query_type),
639 plan_type: format!("{:?}", plan.plan_type),
640 estimated_cost: plan.estimated_cost,
641 estimated_rows: plan.estimated_rows,
642 selected_indexes: plan
643 .selected_indexes
644 .iter()
645 .map(|idx| format!("{} ({:?})", idx.index_name, idx.index_type))
646 .collect(),
647 execution_steps: plan
648 .steps
649 .iter()
650 .map(|step| {
651 format!(
652 "{:?}: {} (cost: {:.2})",
653 step.step_type,
654 step.columns.join(", "),
655 step.cost
656 )
657 })
658 .collect(),
659 parallelization_info: plan
660 .steps
661 .iter()
662 .filter(|step| step.parallelization.can_parallelize)
663 .map(|step| {
664 format!(
665 "Threads: {}, Partition: {:?}",
666 step.parallelization.suggested_threads, step.parallelization.partition_key
667 )
668 })
669 .collect(),
670 })
671 }
672
673 pub async fn analyze(&self, cql: &str) -> Result<AnalyzeResult> {
675 let start_time = Instant::now();
676
677 let mut execution_times = Vec::new();
679 let mut results = Vec::new();
680
681 for _ in 0..self.config.query.analyze_iterations.unwrap_or(5) {
682 let iter_start = Instant::now();
683 let result = self.execute(cql).await?;
684 execution_times.push(iter_start.elapsed());
685 results.push(result);
686 }
687
688 let total_time = start_time.elapsed();
689 let avg_time =
690 execution_times.iter().sum::<std::time::Duration>() / execution_times.len() as u32;
691 let no_times = || Error::query_execution("No execution times recorded for analysis");
692 let min_time = execution_times.iter().min().ok_or_else(no_times)?;
693 let max_time = execution_times.iter().max().ok_or_else(no_times)?;
694
695 let variance = execution_times
697 .iter()
698 .map(|time| {
699 let diff = time.as_nanos() as f64 - avg_time.as_nanos() as f64;
700 diff * diff
701 })
702 .sum::<f64>()
703 / execution_times.len() as f64;
704 let std_dev = variance.sqrt();
705
706 Ok(AnalyzeResult {
707 iterations: execution_times.len(),
708 total_time_ms: total_time.as_millis() as u64,
709 avg_time_ms: avg_time.as_millis() as u64,
710 min_time_ms: min_time.as_millis() as u64,
711 max_time_ms: max_time.as_millis() as u64,
712 std_dev_ms: (std_dev / 1_000_000.0) as u64, avg_rows_returned: results.iter().map(|r| r.rows.len()).sum::<usize>() / results.len(),
714 cache_hit_ratio: self.stats().cache_hit_ratio,
715 })
716 }
717
718 fn cache_query_plan(
720 &self,
721 cql: &str,
722 parsed_query: super::ParsedQuery,
723 plan: super::planner::QueryPlan,
724 ) {
725 let cache_size = self.config.query.query_cache_size.unwrap_or(0);
726 if cache_size == 0 {
727 return;
728 }
729
730 if self.plan_cache.len() >= cache_size {
731 let oldest_key = self
732 .plan_cache
733 .iter()
734 .min_by_key(|entry| entry.cached_at)
735 .map(|entry| entry.key().clone());
736 if let Some(key) = oldest_key {
737 self.plan_cache.remove(&key);
738 }
739 }
740
741 self.plan_cache.insert(
742 cql.to_string(),
743 QueryCacheEntry {
744 parsed_query,
745 plan,
746 cached_at: Instant::now(),
747 hit_count: AtomicU64::new(0),
748 },
749 );
750 }
751
752 pub async fn has_schema_for_table(&self, table: &str) -> bool {
754 self.schema_manager.get_table_schema(table).await.is_ok()
755 }
756
757 pub async fn schema_status(&self, table: &str) -> SchemaStatus {
759 match self.schema_manager.get_table_schema(table).await {
760 Ok(schema) => SchemaStatus::Available {
761 keyspace: schema.keyspace.clone(),
762 table: schema.table.clone(),
763 },
764 Err(Error::Schema(msg)) if msg.contains("not found") => {
765 SchemaStatus::Missing {
766 table: table.to_string(),
767 reason: msg,
768 }
769 }
770 Err(e) => SchemaStatus::ExtractionFailed {
771 table: table.to_string(),
772 cause: e.to_string(),
773 suggestion: "Verify SSTable files are valid Cassandra 5.0 format and Statistics.db contains SerializationHeader".to_string(),
774 },
775 }
776 }
777
778 fn update_execution_stats(&self, result: &mut QueryResult, start_time: Instant) {
790 use crate::observability::{self as obs, catalog, AttrValue};
791
792 let execution_time = start_time.elapsed();
793 result.execution_time_ms = if execution_time.is_zero() {
795 0
796 } else {
797 std::cmp::max(1, execution_time.as_millis() as u64)
798 };
799
800 let access_path_label: &'static str = result
806 .metadata
807 .access_path
808 .as_ref()
809 .map(|p| p.label())
810 .unwrap_or("unknown");
811 let plan_type_label: &'static str = result
812 .metadata
813 .plan_info
814 .as_ref()
815 .map(|p| Self::plan_type_label(&p.plan_type))
816 .unwrap_or("unknown");
817
818 obs::record_histogram(
820 catalog::QUERY_DURATION,
821 execution_time.as_secs_f64(),
822 &[
823 (catalog::attr::SUBSYSTEM, AttrValue::StaticStr("query")),
824 (
825 catalog::attr::ACCESS_PATH,
826 AttrValue::StaticStr(access_path_label),
827 ),
828 (
829 catalog::attr::PLAN_TYPE,
830 AttrValue::StaticStr(plan_type_label),
831 ),
832 ],
833 );
834 obs::add_counter(
835 catalog::QUERY_ROWS,
836 result.rows.len() as u64,
837 &[
838 (
839 catalog::attr::ACCESS_PATH,
840 AttrValue::StaticStr(access_path_label),
841 ),
842 (
843 catalog::attr::PLAN_TYPE,
844 AttrValue::StaticStr(plan_type_label),
845 ),
846 ],
847 );
848
849 let span = tracing::Span::current();
852 span.record(catalog::attr::PLAN_TYPE, plan_type_label);
853 span.record(catalog::attr::ACCESS_PATH, access_path_label);
854 span.record("cqlite.query.rows", result.rows.len());
855
856 let new_time_us = execution_time.as_micros() as u64;
860 self.stats
861 .record_execution(new_time_us, result.rows_affected);
862 }
863
864 fn plan_type_label(plan_type: &str) -> &'static str {
869 match plan_type {
870 "TableScan" => "table_scan",
871 "IndexScan" => "index_scan",
872 "PointLookup" => "point_lookup",
873 "RangeScan" => "range_scan",
874 "Join" => "join",
875 "Aggregation" => "aggregation",
876 "Subquery" => "subquery",
877 _ => "other",
878 }
879 }
880}
881
882#[derive(Debug, Clone)]
884pub struct CacheStats {
885 pub prepared_cache_size: usize,
887 pub plan_cache_size: usize,
889 pub prepared_cache_hits: u64,
891 pub plan_cache_hits: u64,
893}
894
895#[derive(Debug, Clone)]
897pub struct ExplainResult {
898 pub query_type: String,
900 pub plan_type: String,
902 pub estimated_cost: f64,
904 pub estimated_rows: u64,
906 pub selected_indexes: Vec<String>,
908 pub execution_steps: Vec<String>,
910 pub parallelization_info: Vec<String>,
912}
913
914#[derive(Debug, Clone)]
916pub struct AnalyzeResult {
917 pub iterations: usize,
919 pub total_time_ms: u64,
921 pub avg_time_ms: u64,
923 pub min_time_ms: u64,
925 pub max_time_ms: u64,
927 pub std_dev_ms: u64,
929 pub avg_rows_returned: usize,
931 pub cache_hit_ratio: f64,
933}
934
935#[cfg(all(test, feature = "state_machine"))]
936mod tests {
937 use super::*;
938 use crate::Config;
939 use std::sync::Arc;
940 use tempfile::TempDir;
941
942 #[tokio::test]
943 async fn test_query_engine_creation() {
944 let temp_dir = TempDir::new().unwrap();
945 let config = Config::default();
946 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
947
948 let storage = Arc::new(
949 crate::storage::StorageEngine::open(
950 temp_dir.path(),
951 &config,
952 platform,
953 #[cfg(feature = "state_machine")]
954 None,
955 )
956 .await
957 .unwrap(),
958 );
959 let schema = Arc::new(
960 crate::schema::SchemaManager::new(temp_dir.path())
961 .await
962 .unwrap(),
963 );
964 let memory = Arc::new(crate::memory::MemoryManager::new(&config).unwrap());
965
966 let query_engine = QueryEngine::new(storage, schema, memory, &config).unwrap();
967
968 assert_eq!(query_engine.stats().total_queries, 0);
969 assert_eq!(query_engine.cache_stats().prepared_cache_size, 0);
970 assert_eq!(query_engine.cache_stats().plan_cache_size, 0);
971 }
972
973 #[tokio::test]
974 #[ignore = "Hangs >60s; needs investigation - gated for M1"]
975 async fn test_query_caching() {
976 let temp_dir = TempDir::new().unwrap();
977 let mut config = Config::test_config();
978 config.query.query_cache_size = Some(10);
979
980 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
981 let storage = Arc::new(
982 crate::storage::StorageEngine::open(
983 temp_dir.path(),
984 &config,
985 platform,
986 #[cfg(feature = "state_machine")]
987 None,
988 )
989 .await
990 .unwrap(),
991 );
992 let schema = Arc::new(
993 crate::schema::SchemaManager::new(temp_dir.path())
994 .await
995 .unwrap(),
996 );
997 let memory = Arc::new(crate::memory::MemoryManager::new(&config).unwrap());
998
999 let query_engine = QueryEngine::new(storage, schema, memory, &config).unwrap();
1000
1001 let cql = "SELECT * FROM users WHERE id = 1";
1003 let _ = query_engine.execute(cql).await;
1004 let _ = query_engine.execute(cql).await;
1005
1006 assert_eq!(query_engine.cache_stats().plan_cache_size, 1);
1008
1009 let stats = query_engine.stats();
1011 assert!(stats.cache_hit_ratio > 0.0);
1012 }
1013
1014 #[tokio::test]
1015 #[cfg(feature = "state_machine")]
1016 async fn test_prepared_statements() {
1017 let temp_dir = TempDir::new().unwrap();
1018 let config = Config::default();
1019 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1020
1021 let storage = Arc::new(
1022 crate::storage::StorageEngine::open(
1023 temp_dir.path(),
1024 &config,
1025 platform,
1026 #[cfg(feature = "state_machine")]
1027 None,
1028 )
1029 .await
1030 .unwrap(),
1031 );
1032 let schema = Arc::new(
1033 crate::schema::SchemaManager::new(temp_dir.path())
1034 .await
1035 .unwrap(),
1036 );
1037 let memory = Arc::new(crate::memory::MemoryManager::new(&config).unwrap());
1038
1039 let query_engine = QueryEngine::new(storage, schema, memory, &config).unwrap();
1040
1041 let cql = "SELECT * FROM users WHERE id = ?";
1043 let prepared = query_engine.prepare(cql).await.unwrap();
1044
1045 let params = vec![Value::Integer(1)];
1047 let result = query_engine
1048 .execute_prepared(&prepared, ¶ms)
1049 .await
1050 .unwrap();
1051
1052 assert!(result.execution_time_ms > 0);
1054
1055 assert_eq!(query_engine.cache_stats().prepared_cache_size, 1);
1057 }
1058
1059 #[tokio::test]
1060 async fn test_query_explain() {
1061 let temp_dir = TempDir::new().unwrap();
1062 let config = Config::default();
1063 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1064
1065 let storage = Arc::new(
1066 crate::storage::StorageEngine::open(
1067 temp_dir.path(),
1068 &config,
1069 platform,
1070 #[cfg(feature = "state_machine")]
1071 None,
1072 )
1073 .await
1074 .unwrap(),
1075 );
1076 let schema = Arc::new(
1077 crate::schema::SchemaManager::new(temp_dir.path())
1078 .await
1079 .unwrap(),
1080 );
1081 let memory = Arc::new(crate::memory::MemoryManager::new(&config).unwrap());
1082
1083 let query_engine = QueryEngine::new(storage, schema, memory, &config).unwrap();
1084
1085 let cql = "SELECT * FROM users WHERE id = 1";
1087 let explain_result = query_engine.explain(cql).await.unwrap();
1088
1089 assert_eq!(explain_result.query_type, "Select");
1090 assert!(explain_result.estimated_cost > 0.0);
1091 assert!(!explain_result.selected_indexes.is_empty());
1092 assert!(!explain_result.execution_steps.is_empty());
1093 }
1094
1095 #[tokio::test]
1096 #[cfg(feature = "state_machine")]
1097 async fn test_cache_eviction() {
1098 let temp_dir = TempDir::new().unwrap();
1099 let mut config = Config::default();
1100 config.query.query_cache_size = Some(2); let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1103 let storage = Arc::new(
1104 crate::storage::StorageEngine::open(
1105 temp_dir.path(),
1106 &config,
1107 platform,
1108 #[cfg(feature = "state_machine")]
1109 None,
1110 )
1111 .await
1112 .unwrap(),
1113 );
1114 let schema = Arc::new(
1115 crate::schema::SchemaManager::new(temp_dir.path())
1116 .await
1117 .unwrap(),
1118 );
1119 let memory = Arc::new(crate::memory::MemoryManager::new(&config).unwrap());
1120
1121 let query_engine = QueryEngine::new(storage, schema, memory, &config).unwrap();
1122
1123 let _ = query_engine
1126 .execute("SELECT * FROM users WHERE id = 1")
1127 .await;
1128 let _ = query_engine
1129 .execute("SELECT * FROM users WHERE id = 2")
1130 .await;
1131 let _ = query_engine
1132 .execute("SELECT * FROM users WHERE id = 3")
1133 .await;
1134
1135 assert_eq!(query_engine.select_plan_cache.len(), 2);
1137 }
1138
1139 #[tokio::test]
1140 #[cfg(feature = "state_machine")]
1141 async fn test_schema_validation_api() {
1142 let temp_dir = TempDir::new().unwrap();
1143 let config = Config::default();
1144 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1145
1146 let storage = Arc::new(
1147 crate::storage::StorageEngine::open(
1148 temp_dir.path(),
1149 &config,
1150 platform,
1151 #[cfg(feature = "state_machine")]
1152 None,
1153 )
1154 .await
1155 .unwrap(),
1156 );
1157 let schema = Arc::new(
1158 crate::schema::SchemaManager::new(temp_dir.path())
1159 .await
1160 .unwrap(),
1161 );
1162 let memory = Arc::new(crate::memory::MemoryManager::new(&config).unwrap());
1163
1164 let query_engine = QueryEngine::new(storage, schema, memory, &config).unwrap();
1165
1166 let has_schema = query_engine.has_schema_for_table("nonexistent_table").await;
1168 assert!(!has_schema, "Should return false for non-existent table");
1169
1170 let status = query_engine.schema_status("nonexistent_table").await;
1172 match status {
1173 SchemaStatus::Missing { .. } | SchemaStatus::ExtractionFailed { .. } => {
1174 }
1176 SchemaStatus::Available { .. } => {
1177 panic!("Should not be Available for non-existent table");
1178 }
1179 }
1180 }
1181
1182 #[tokio::test]
1193 #[cfg(feature = "state_machine")]
1194 async fn adhoc_select_reuses_cached_optimized_plan() {
1195 use crate::query::select_optimizer::OPTIMIZE_INVOCATIONS;
1196
1197 let temp_dir = TempDir::new().unwrap();
1198 let mut config = Config::default();
1199 config.query.query_cache_size = Some(16);
1201 let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
1202 let storage = Arc::new(
1203 crate::storage::StorageEngine::open(
1204 temp_dir.path(),
1205 &config,
1206 platform,
1207 #[cfg(feature = "state_machine")]
1208 None,
1209 )
1210 .await
1211 .unwrap(),
1212 );
1213 let schema = Arc::new(
1214 crate::schema::SchemaManager::new(temp_dir.path())
1215 .await
1216 .unwrap(),
1217 );
1218 let memory = Arc::new(crate::memory::MemoryManager::new(&config).unwrap());
1219 let engine = QueryEngine::new(storage, schema, memory, &config).unwrap();
1220
1221 let cql = "SELECT * FROM t WHERE age > 5";
1224
1225 OPTIMIZE_INVOCATIONS.with(|c| c.set(0));
1226 for _ in 0..75 {
1227 let _ = engine.execute(cql).await;
1228 }
1229 let after_same = OPTIMIZE_INVOCATIONS.with(|c| c.get());
1230 assert!(
1231 after_same <= 1,
1232 "issue #1587: an ad-hoc SELECT must optimize at most once across 75 \
1233 identical executes (modern plan cache), got {after_same}"
1234 );
1235
1236 engine.clear_plan_cache();
1238 let _ = engine.execute(cql).await;
1239 let after_clear = OPTIMIZE_INVOCATIONS.with(|c| c.get());
1240 assert_eq!(
1241 after_clear,
1242 after_same + 1,
1243 "clearing the plan cache must re-optimize once (no stale reuse)"
1244 );
1245 }
1246}
1247
1248#[cfg(test)]
1254#[path = "engine_lock_hygiene_tests.rs"]
1255mod engine_lock_hygiene_tests;
1256
1257#[cfg(test)]
1258#[cfg(all(feature = "experimental", feature = "state_machine"))]
1259mod plan_cache_tests {
1260 use super::*;
1261 use crate::{
1262 memory::MemoryManager, platform::Platform, schema::SchemaManager, storage::StorageEngine,
1263 Config,
1264 };
1265 use std::sync::Arc;
1266 use tempfile::TempDir;
1267
1268 async fn setup_query_engine(config: &Config) -> (QueryEngine, TempDir) {
1269 let temp_dir = TempDir::new().unwrap();
1270 let platform = Arc::new(Platform::new(config).await.unwrap());
1271 let storage = Arc::new(
1272 StorageEngine::open(
1273 temp_dir.path(),
1274 config,
1275 platform,
1276 #[cfg(feature = "state_machine")]
1277 None,
1278 )
1279 .await
1280 .unwrap(),
1281 );
1282 let schema = Arc::new(SchemaManager::new(temp_dir.path()).await.unwrap());
1283 let memory = Arc::new(MemoryManager::new(config).unwrap());
1284
1285 let engine = QueryEngine::new(storage, schema, memory, config).unwrap();
1286 (engine, temp_dir)
1287 }
1288
1289 fn select_plan_cache_len(engine: &QueryEngine) -> usize {
1294 engine.select_plan_cache.len()
1295 }
1296
1297 async fn create_sample_table(engine: &QueryEngine) {
1302 engine
1303 .execute(
1304 "CREATE TABLE plan_cache_test (
1305 id INTEGER PRIMARY KEY,
1306 value TEXT
1307 )",
1308 )
1309 .await
1310 .unwrap();
1311 }
1312
1313 #[tokio::test]
1314 async fn test_plan_cache_disabled() {
1315 let mut config = Config::default();
1316 config.query.query_cache_size = Some(0);
1317
1318 let (engine, _temp_dir) = setup_query_engine(&config).await;
1319 create_sample_table(&engine).await;
1320
1321 engine
1322 .execute("SELECT * FROM plan_cache_test WHERE id = 1")
1323 .await
1324 .unwrap();
1325
1326 assert_eq!(select_plan_cache_len(&engine), 0);
1327 }
1328
1329 #[tokio::test]
1330 async fn test_plan_cache_reuse_point_lookup() {
1331 let mut config = Config::default();
1332 config.query.query_cache_size = Some(4);
1333
1334 let (engine, _temp_dir) = setup_query_engine(&config).await;
1335 create_sample_table(&engine).await;
1336
1337 engine.clear_plan_cache();
1338
1339 engine
1340 .execute("SELECT * FROM plan_cache_test WHERE id = 1")
1341 .await
1342 .unwrap();
1343 engine
1344 .execute("SELECT * FROM plan_cache_test WHERE id = 1")
1345 .await
1346 .unwrap();
1347
1348 assert_eq!(select_plan_cache_len(&engine), 1);
1349 assert!(engine.stats().cache_hit_ratio > 0.0);
1350 }
1351
1352 #[tokio::test]
1353 async fn test_plan_cache_eviction_limit() {
1354 let mut config = Config::default();
1355 config.query.query_cache_size = Some(2);
1356
1357 let (engine, _temp_dir) = setup_query_engine(&config).await;
1358 create_sample_table(&engine).await;
1359
1360 engine.clear_plan_cache();
1361
1362 for id in 1..=3 {
1363 engine
1364 .execute(&format!("SELECT * FROM plan_cache_test WHERE id = {}", id))
1365 .await
1366 .unwrap();
1367 }
1368
1369 assert_eq!(select_plan_cache_len(&engine), 2);
1370 }
1371}