1use std::collections::HashMap;
16use std::fmt;
17use std::path::PathBuf;
18use std::sync::Arc;
19use std::time::Instant;
20
21use clap::Parser;
22use colored::Colorize;
23use common_base::Plugins;
24use common_error::ext::{BoxedError, PlainError};
25use common_error::status_code::StatusCode;
26use common_meta::cache::{new_schema_cache, new_table_schema_cache};
27use common_meta::key::SchemaMetadataManager;
28use common_meta::kv_backend::memory::MemoryKvBackend;
29use common_wal::config::DatanodeWalConfig;
30use datafusion::execution::SessionStateBuilder;
31use datafusion::logical_expr::{BinaryExpr, Expr as DfExpr, ExprSchemable, Operator};
32use datafusion_common::tree_node::{Transformed, TreeNodeRewriter};
33use datafusion_common::{DFSchemaRef, ScalarValue, ToDFSchema};
34use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet;
35use datafusion_physical_plan::{DisplayAs, DisplayFormatType};
36use datatypes::arrow::compute;
37use futures::StreamExt;
38use futures::stream::FuturesUnordered;
39use log_store::kafka::log_store::KafkaLogStore;
40use log_store::noop::log_store::NoopLogStore;
41use log_store::raft_engine::log_store::RaftEngineLogStore;
42use mito2::config::MitoConfig;
43use mito2::engine::MitoEngine;
44use mito2::sst::file_ref::FileReferenceManager;
45use moka::future::CacheBuilder;
46use object_store::manager::ObjectStoreManager;
47use object_store::util::normalize_dir;
48use query::optimizer::parallelize_scan::ParallelizeScan;
49use serde::Deserialize;
50use snafu::{OptionExt, ResultExt};
51use sqlparser::ast::ExprWithAlias as SqlExprWithAlias;
52use sqlparser::dialect::GenericDialect;
53use sqlparser::parser::Parser as SqlParser;
54use store_api::metadata::RegionMetadata;
55use store_api::path_utils::WAL_DIR;
56use store_api::region_engine::{PrepareRequest, QueryScanContext, RegionEngine};
57use store_api::region_request::{RegionOpenRequest, RegionRequest};
58use store_api::storage::{RegionId, ScanRequest, TimeSeriesDistribution, TimeSeriesRowSelector};
59use tokio::fs;
60
61use crate::datanode::tool_util::{
62 build_object_store, format_bytes, parse_config, parse_path_type, parse_region_id,
63};
64use crate::error;
65
66struct VerboseScannerDisplay<'a, T: ?Sized>(&'a T);
68
69impl<T: DisplayAs + ?Sized> fmt::Display for VerboseScannerDisplay<'_, T> {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 self.0.fmt_as(DisplayFormatType::Verbose, f)
72 }
73}
74
75#[derive(Debug, Parser)]
77pub struct ScanbenchCommand {
78 #[clap(long, value_name = "FILE")]
80 config: PathBuf,
81
82 #[clap(long)]
84 region_id: String,
85
86 #[clap(long)]
88 table_dir: String,
89
90 #[clap(long, default_value = "seq")]
92 scanner: String,
93
94 #[clap(long, value_name = "FILE")]
96 scan_config: Option<PathBuf>,
97
98 #[clap(long, default_value = "1")]
100 parallelism: usize,
101
102 #[clap(long, default_value = "1")]
104 iterations: usize,
105
106 #[clap(long, default_value = "bare")]
108 path_type: String,
109
110 #[clap(short, long, default_value_t = false)]
112 verbose: bool,
113
114 #[clap(long, value_name = "FILE")]
116 pprof_file: Option<PathBuf>,
117
118 #[clap(long, default_value_t = false)]
120 enable_wal: bool,
121
122 #[clap(long, default_value_t = false)]
124 pprof_after_warmup: bool,
125}
126
127#[derive(Debug, Deserialize, Default)]
129struct ScanConfig {
130 projection: Option<Vec<usize>>,
131 projection_names: Option<Vec<String>>,
132 filters: Option<Vec<String>>,
133 series_row_selector: Option<String>,
134}
135
136fn resolve_projection(
137 scan_config: &ScanConfig,
138 metadata: Option<&RegionMetadata>,
139) -> error::Result<Option<Vec<usize>>> {
140 if scan_config.projection.is_some() && scan_config.projection_names.is_some() {
141 return Err(error::IllegalConfigSnafu {
142 msg: "scan config cannot contain both 'projection' and 'projection_names'".to_string(),
143 }
144 .build());
145 }
146
147 if let Some(projection) = &scan_config.projection {
148 return Ok(Some(projection.clone()));
149 }
150
151 if let Some(projection_names) = &scan_config.projection_names {
152 let metadata = metadata.context(error::IllegalConfigSnafu {
153 msg: "Missing region metadata while resolving 'projection_names'".to_string(),
154 })?;
155 let available_columns = metadata
156 .column_metadatas
157 .iter()
158 .map(|column| column.column_schema.name.as_str())
159 .collect::<Vec<_>>()
160 .join(", ");
161 let projection = projection_names
162 .iter()
163 .map(|name| {
164 metadata
165 .column_index_by_name(name)
166 .with_context(|| error::IllegalConfigSnafu {
167 msg: format!(
168 "Unknown column '{}' in projection_names, available columns: [{}]",
169 name, available_columns
170 ),
171 })
172 })
173 .collect::<error::Result<Vec<_>>>()?;
174 return Ok(Some(projection));
175 }
176
177 Ok(None)
178}
179
180struct LiteralTypeCaster {
182 schema: DFSchemaRef,
183}
184
185impl TreeNodeRewriter for LiteralTypeCaster {
186 type Node = DfExpr;
187
188 fn f_up(&mut self, expr: DfExpr) -> datafusion_common::Result<Transformed<DfExpr>> {
189 let DfExpr::BinaryExpr(BinaryExpr { left, op, right }) = &expr else {
190 return Ok(Transformed::no(expr));
191 };
192
193 if !matches!(
194 op,
195 Operator::Eq
196 | Operator::NotEq
197 | Operator::Lt
198 | Operator::LtEq
199 | Operator::Gt
200 | Operator::GtEq
201 ) {
202 return Ok(Transformed::no(expr));
203 }
204
205 let (col_expr, lit_expr, col_left) = match (left.as_ref(), right.as_ref()) {
206 (col @ DfExpr::Column(_), lit @ DfExpr::Literal(_, _)) => (col, lit, true),
207 (lit @ DfExpr::Literal(_, _), col @ DfExpr::Column(_)) => (col, lit, false),
208 _ => return Ok(Transformed::no(expr)),
209 };
210
211 let col_type = col_expr.get_type(self.schema.as_ref())?;
212 let DfExpr::Literal(scalar, _) = lit_expr else {
213 unreachable!()
214 };
215
216 if scalar.data_type() == col_type {
217 return Ok(Transformed::no(expr));
218 }
219
220 let lit_array = scalar.to_array()?;
221 let casted = compute::cast(lit_array.as_ref(), &col_type).map_err(|e| {
222 datafusion_common::DataFusionError::Internal(format!(
223 "Failed to cast literal {:?} to {:?}: {}",
224 scalar, col_type, e
225 ))
226 })?;
227 let casted_scalar = ScalarValue::try_from_array(&casted, 0)?;
228
229 let new_lit = DfExpr::Literal(casted_scalar, None);
230 let (new_left, new_right) = if col_left {
231 (left.clone(), Box::new(new_lit))
232 } else {
233 (Box::new(new_lit), right.clone())
234 };
235
236 Ok(Transformed::yes(DfExpr::BinaryExpr(BinaryExpr {
237 left: new_left,
238 op: *op,
239 right: new_right,
240 })))
241 }
242}
243
244fn convert_literal_types(
245 exprs: Vec<DfExpr>,
246 schema: &DFSchemaRef,
247) -> datafusion_common::Result<Vec<DfExpr>> {
248 use datafusion_common::tree_node::TreeNode;
249
250 let mut caster = LiteralTypeCaster {
251 schema: schema.clone(),
252 };
253 exprs
254 .into_iter()
255 .map(|e| e.rewrite(&mut caster).map(|x| x.data))
256 .collect()
257}
258
259fn resolve_filters(
260 scan_config: &ScanConfig,
261 metadata: &RegionMetadata,
262) -> error::Result<Vec<DfExpr>> {
263 let Some(filters) = &scan_config.filters else {
264 return Ok(Vec::new());
265 };
266
267 let df_schema = metadata
268 .schema
269 .arrow_schema()
270 .clone()
271 .to_dfschema()
272 .map_err(|e| {
273 error::IllegalConfigSnafu {
274 msg: format!("Failed to convert region schema to DataFusion schema: {e}"),
275 }
276 .build()
277 })?;
278
279 let state = SessionStateBuilder::new()
280 .with_config(Default::default())
281 .with_runtime_env(Default::default())
282 .with_default_features()
283 .build();
284
285 let exprs: Vec<DfExpr> = filters
286 .iter()
287 .enumerate()
288 .map(|(idx, filter)| {
289 let mut parser = SqlParser::new(&GenericDialect {})
290 .try_with_sql(filter)
291 .map_err(|e| {
292 error::IllegalConfigSnafu {
293 msg: format!("Invalid filter at index {idx} ('{filter}'): {e}"),
294 }
295 .build()
296 })?;
297
298 let sql_expr = parser.parse_expr().map_err(|e| {
299 error::IllegalConfigSnafu {
300 msg: format!("Invalid filter at index {idx} ('{filter}'): {e}"),
301 }
302 .build()
303 })?;
304
305 state
306 .create_logical_expr_from_sql_expr(
307 SqlExprWithAlias {
308 expr: sql_expr,
309 alias: None,
310 },
311 &df_schema,
312 )
313 .map_err(|e| {
314 error::IllegalConfigSnafu {
315 msg: format!(
316 "Failed to convert filter at index {idx} ('{filter}') to logical expr: {e}"
317 ),
318 }
319 .build()
320 })
321 })
322 .collect::<error::Result<Vec<_>>>()?;
323
324 let df_schema_ref = Arc::new(df_schema);
325 convert_literal_types(exprs, &df_schema_ref).map_err(|e| {
326 error::IllegalConfigSnafu {
327 msg: format!("Failed to convert filter expression types: {e}"),
328 }
329 .build()
330 })
331}
332
333fn noop_partition_expr_fetcher() -> mito2::region::opener::PartitionExprFetcherRef {
334 struct NoopPartitionExprFetcher;
335
336 #[async_trait::async_trait]
337 impl mito2::region::opener::PartitionExprFetcher for NoopPartitionExprFetcher {
338 async fn fetch_expr(&self, _region_id: RegionId) -> Option<String> {
339 None
340 }
341 }
342
343 Arc::new(NoopPartitionExprFetcher)
344}
345
346struct EngineComponents {
347 data_home: String,
348 mito_config: MitoConfig,
349 object_store_manager: Arc<ObjectStoreManager>,
350 schema_metadata_manager: Arc<SchemaMetadataManager>,
351 file_ref_manager: Arc<FileReferenceManager>,
352 partition_expr_fetcher: mito2::region::opener::PartitionExprFetcherRef,
353}
354
355impl EngineComponents {
356 async fn build<S: store_api::logstore::LogStore>(
357 self,
358 log_store: Arc<S>,
359 ) -> error::Result<MitoEngine> {
360 MitoEngine::new(
361 &self.data_home,
362 self.mito_config,
363 log_store,
364 self.object_store_manager,
365 self.schema_metadata_manager,
366 self.file_ref_manager,
367 self.partition_expr_fetcher,
368 Plugins::default(),
369 )
370 .await
371 .map_err(BoxedError::new)
372 .context(error::BuildCliSnafu)
373 }
374}
375
376fn mock_schema_metadata_manager() -> Arc<SchemaMetadataManager> {
377 let kv_backend = Arc::new(MemoryKvBackend::new());
378 let table_schema_cache = Arc::new(new_table_schema_cache(
379 "table_schema_name_cache".to_string(),
380 CacheBuilder::default().build(),
381 kv_backend.clone(),
382 ));
383 let schema_cache = Arc::new(new_schema_cache(
384 "schema_cache".to_string(),
385 CacheBuilder::default().build(),
386 kv_backend.clone(),
387 ));
388 Arc::new(SchemaMetadataManager::new(table_schema_cache, schema_cache))
389}
390
391impl ScanbenchCommand {
392 pub async fn run(&self) -> error::Result<()> {
393 if self.verbose {
394 common_telemetry::init_default_ut_logging();
395 }
396
397 println!("{}", "Starting scanbench...".cyan().bold());
398
399 let region_id = parse_region_id(&self.region_id)?;
400 let path_type = parse_path_type(&self.path_type)?;
401 println!(
402 "{} Region ID: {} (u64: {})",
403 "✓".green(),
404 self.region_id,
405 region_id.as_u64()
406 );
407
408 let (store_cfg, mito_config, wal_config) = parse_config(&self.config)?;
410 println!("{} Config parsed", "✓".green());
411
412 let object_store = build_object_store(&store_cfg).await?;
413 println!("{} Object store initialized", "✓".green());
414
415 let object_store_manager =
416 Arc::new(ObjectStoreManager::new("default", object_store.clone()));
417
418 let schema_metadata_manager = mock_schema_metadata_manager();
420 let file_ref_manager = Arc::new(FileReferenceManager::new(None));
421 let partition_expr_fetcher = noop_partition_expr_fetcher();
422
423 let components = EngineComponents {
425 data_home: store_cfg.data_home.clone(),
426 mito_config,
427 object_store_manager,
428 schema_metadata_manager,
429 file_ref_manager,
430 partition_expr_fetcher,
431 };
432
433 let engine = match &wal_config {
434 DatanodeWalConfig::RaftEngine(raft_engine_config) if self.enable_wal => {
435 let data_home = normalize_dir(&store_cfg.data_home);
436 let wal_dir = match &raft_engine_config.dir {
437 Some(dir) => dir.clone(),
438 None => format!("{}{WAL_DIR}", data_home),
439 };
440 fs::create_dir_all(&wal_dir).await.map_err(|e| {
441 error::IllegalConfigSnafu {
442 msg: format!("failed to create WAL directory {}: {e}", wal_dir),
443 }
444 .build()
445 })?;
446 let log_store = Arc::new(
447 RaftEngineLogStore::try_new(wal_dir, raft_engine_config)
448 .await
449 .map_err(BoxedError::new)
450 .context(error::BuildCliSnafu)?,
451 );
452 println!("{} Using RaftEngine WAL", "✓".green());
453 components.build(log_store).await?
454 }
455 DatanodeWalConfig::Kafka(kafka_config) if self.enable_wal => {
456 let log_store = Arc::new(
457 KafkaLogStore::try_new(kafka_config, None)
458 .await
459 .map_err(BoxedError::new)
460 .context(error::BuildCliSnafu)?,
461 );
462 println!("{} Using Kafka WAL", "✓".green());
463 components.build(log_store).await?
464 }
465 _ => {
466 let log_store = Arc::new(NoopLogStore);
467 println!(
468 "{} Using NoopLogStore (enable_wal={})",
469 "✓".green(),
470 self.enable_wal
471 );
472 components.build(log_store).await?
473 }
474 };
475
476 let open_request = RegionOpenRequest {
478 engine: "mito".to_string(),
479 table_dir: self.table_dir.clone(),
480 path_type,
481 options: HashMap::default(),
482 skip_wal_replay: !self.enable_wal,
483 checkpoint: None,
484 requirements: Default::default(),
485 };
486
487 engine
488 .handle_request(region_id, RegionRequest::Open(open_request))
489 .await
490 .map_err(BoxedError::new)
491 .context(error::BuildCliSnafu)?;
492 println!("{} Region opened", "✓".green());
493
494 let scan_config = if let Some(path) = &self.scan_config {
496 let content = tokio::fs::read_to_string(path)
497 .await
498 .context(error::FileIoSnafu)?;
499 serde_json::from_str::<ScanConfig>(&content).context(error::SerdeJsonSnafu)?
500 } else {
501 ScanConfig::default()
502 };
503 let metadata = engine
504 .get_metadata(region_id)
505 .await
506 .map_err(BoxedError::new)
507 .context(error::BuildCliSnafu)?;
508 let projection = resolve_projection(&scan_config, Some(&metadata))?;
509 let filters = resolve_filters(&scan_config, &metadata)?;
510
511 let distribution = match self.scanner.as_str() {
513 "seq" => None,
514 "unordered" => Some(TimeSeriesDistribution::TimeWindowed),
515 "series" => Some(TimeSeriesDistribution::PerSeries),
516 other => {
517 return Err(error::IllegalConfigSnafu {
518 msg: format!(
519 "Unknown scanner type '{}', expected: seq, unordered, series",
520 other
521 ),
522 }
523 .build());
524 }
525 };
526
527 let series_row_selector = match scan_config.series_row_selector.as_deref() {
528 Some("last_row") => Some(TimeSeriesRowSelector::LastRow),
529 Some(other) => {
530 return Err(error::IllegalConfigSnafu {
531 msg: format!("Unknown series_row_selector '{}'", other),
532 }
533 .build());
534 }
535 None => None,
536 };
537
538 println!(
539 "{} Scanner: {}, Parallelism: {}, Iterations: {}",
540 "ℹ".blue(),
541 self.scanner,
542 self.parallelism,
543 self.iterations,
544 );
545
546 #[cfg(unix)]
548 let mut profiler_guard = if self.pprof_file.is_some() && !self.pprof_after_warmup {
549 println!("{} Starting profiling...", "⚡".yellow());
550 Some(
551 pprof::ProfilerGuardBuilder::default()
552 .frequency(99)
553 .blocklist(&["libc", "libgcc", "pthread", "vdso"])
554 .build()
555 .map_err(|e| {
556 BoxedError::new(PlainError::new(
557 format!("Failed to start profiler: {e}"),
558 StatusCode::Unexpected,
559 ))
560 })
561 .context(error::BuildCliSnafu)?,
562 )
563 } else {
564 None
565 };
566
567 #[cfg(not(unix))]
568 if self.pprof_file.is_some() {
569 eprintln!(
570 "{}: Profiling is not supported on this platform",
571 "Warning".yellow()
572 );
573 }
574
575 let mut total_rows_all = 0u64;
576 let mut total_elapsed_all = std::time::Duration::ZERO;
577
578 for iteration in 0..self.iterations {
579 let request = ScanRequest {
580 projection: projection.clone(),
581 filters: filters.clone(),
582 series_row_selector,
583 distribution,
584 ..Default::default()
585 };
586
587 let start = Instant::now();
588
589 let mut scanner = engine
591 .handle_query(region_id, request)
592 .await
593 .map_err(BoxedError::new)
594 .context(error::BuildCliSnafu)?;
595
596 let original_partitions = scanner.properties().partitions.clone();
598 let total_ranges: usize = original_partitions.iter().map(|p| p.len()).sum();
599
600 if self.verbose {
601 println!(
602 " {} Original partitions: {}, total ranges: {}",
603 "ℹ".blue(),
604 original_partitions.len(),
605 total_ranges
606 );
607 }
608
609 if self.parallelism > 1 {
610 let all_ranges: Vec<_> = original_partitions.into_iter().flatten().collect();
612
613 let mut partitions =
615 ParallelizeScan::assign_partition_range(all_ranges, self.parallelism);
616
617 for partition in &mut partitions {
619 partition.sort_by_key(|a| a.start);
620 }
621
622 scanner
623 .prepare(
624 PrepareRequest::default()
625 .with_ranges(partitions)
626 .with_target_partitions(self.parallelism),
627 )
628 .map_err(BoxedError::new)
629 .context(error::BuildCliSnafu)?;
630 }
631
632 let num_partitions = scanner.properties().partitions.len();
634 let ctx = QueryScanContext {
635 explain_verbose: self.verbose,
636 };
637 let metrics_set = ExecutionPlanMetricsSet::new();
638
639 let mut scan_futures = FuturesUnordered::new();
640
641 for partition_idx in 0..num_partitions {
642 let mut stream = scanner
643 .scan_partition(&ctx, &metrics_set, partition_idx)
644 .map_err(BoxedError::new)
645 .context(error::BuildCliSnafu)?;
646
647 scan_futures.push(tokio::spawn(async move {
648 let mut rows = 0u64;
649 let mut array_mem_size = 0u64;
650 let mut estimated_size = 0u64;
651 while let Some(batch_result) = stream.next().await {
652 match batch_result {
653 Ok(batch) => {
654 rows += batch.num_rows() as u64;
655 let df_batch = batch.df_record_batch();
656 array_mem_size += df_batch.get_array_memory_size() as u64;
657 estimated_size +=
658 mito2::memtable::record_batch_estimated_size(df_batch) as u64;
659 }
660 Err(e) => {
661 return Err(BoxedError::new(e));
662 }
663 }
664 }
665 Ok::<(u64, u64, u64), BoxedError>((rows, array_mem_size, estimated_size))
666 }));
667 }
668
669 let mut total_rows = 0u64;
670 let mut total_array_mem_size = 0u64;
671 let mut total_estimated_size = 0u64;
672 while let Some(task) = scan_futures.next().await {
673 let result = task
674 .map_err(|e| {
675 BoxedError::new(PlainError::new(
676 format!("scan task failed: {e}"),
677 StatusCode::Unexpected,
678 ))
679 })
680 .context(error::BuildCliSnafu)?;
681 let (rows, array_mem_size, estimated_size) =
682 result.context(error::BuildCliSnafu)?;
683 total_rows += rows;
684 total_array_mem_size += array_mem_size;
685 total_estimated_size += estimated_size;
686 }
687
688 let elapsed = start.elapsed();
689 total_rows_all += total_rows;
690 total_elapsed_all += elapsed;
691
692 println!(
693 " [iter {}] {} rows in {:?} ({} partitions), array_mem_size: {}, estimated_size: {}",
694 iteration + 1,
695 total_rows.to_string().cyan(),
696 elapsed,
697 num_partitions,
698 format_bytes(total_array_mem_size),
699 format_bytes(total_estimated_size),
700 );
701
702 if self.verbose {
703 println!(
704 " {} Scanner explain: {}",
705 "ℹ".blue(),
706 VerboseScannerDisplay(scanner.as_ref())
707 );
708 }
709
710 #[cfg(unix)]
712 if iteration == 0
713 && self.pprof_after_warmup
714 && self.pprof_file.is_some()
715 && profiler_guard.is_none()
716 {
717 println!(
718 "{} Starting profiling after warmup iteration...",
719 "⚡".yellow()
720 );
721 profiler_guard = Some(
722 pprof::ProfilerGuardBuilder::default()
723 .frequency(99)
724 .blocklist(&["libc", "libgcc", "pthread", "vdso"])
725 .build()
726 .map_err(|e| {
727 BoxedError::new(PlainError::new(
728 format!("Failed to start profiler: {e}"),
729 StatusCode::Unexpected,
730 ))
731 })
732 .context(error::BuildCliSnafu)?,
733 );
734 }
735 }
736
737 #[cfg(unix)]
739 if let (Some(guard), Some(pprof_file)) = (profiler_guard, &self.pprof_file) {
740 println!("{} Generating flamegraph...", "🔥".yellow());
741 match guard.report().build() {
742 Ok(report) => {
743 let mut flamegraph_data = Vec::new();
744 if let Err(e) = report.flamegraph(&mut flamegraph_data) {
745 println!("{}: Failed to generate flamegraph: {}", "Error".red(), e);
746 } else if let Err(e) = std::fs::write(pprof_file, flamegraph_data) {
747 println!(
748 "{}: Failed to write flamegraph to {}: {}",
749 "Error".red(),
750 pprof_file.display(),
751 e
752 );
753 } else {
754 println!(
755 "{} Flamegraph saved to {}",
756 "✓".green(),
757 pprof_file.display().to_string().cyan()
758 );
759 }
760 }
761 Err(e) => {
762 println!("{}: Failed to generate pprof report: {}", "Error".red(), e);
763 }
764 }
765 }
766
767 if self.iterations > 1 {
769 let avg_elapsed = total_elapsed_all / self.iterations as u32;
770 let avg_rows = total_rows_all / self.iterations as u64;
771 println!(
772 "\n{} Average: {} rows in {:?} over {} iterations",
773 "Summary".green().bold(),
774 avg_rows.to_string().cyan(),
775 avg_elapsed,
776 self.iterations,
777 );
778 }
779
780 println!("\n{}", "Benchmark completed!".green().bold());
781 Ok(())
782 }
783}
784
785#[cfg(test)]
786mod tests {
787 use datatypes::prelude::ConcreteDataType;
788 use datatypes::schema::ColumnSchema;
789 use sqlparser::ast::{BinaryOperator, Expr};
790 use sqlparser::dialect::GenericDialect;
791 use sqlparser::parser::Parser;
792 use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
793 use store_api::storage::RegionId;
794
795 use super::{ScanConfig, resolve_filters, resolve_projection};
796 use crate::error;
797
798 #[test]
799 fn test_parse_scan_config_projection_names() {
800 let json = r#"{"projection_names":["host","ts"]}"#;
801 let config: ScanConfig = serde_json::from_str(json).unwrap();
802
803 assert_eq!(
804 config.projection_names,
805 Some(vec!["host".to_string(), "ts".to_string()])
806 );
807 assert_eq!(config.projection, None);
808 }
809
810 #[test]
811 fn test_resolve_projection_by_indexes() -> error::Result<()> {
812 let config = ScanConfig {
813 projection: Some(vec![0, 2]),
814 projection_names: None,
815 filters: None,
816 series_row_selector: None,
817 };
818
819 let projection = resolve_projection(&config, None)?;
820 assert_eq!(projection, Some(vec![0, 2]));
821 Ok(())
822 }
823
824 #[test]
825 fn test_resolve_projection_by_names_without_metadata() {
826 let config = ScanConfig {
827 projection: None,
828 projection_names: Some(vec!["cpu".to_string(), "host".to_string()]),
829 filters: None,
830 series_row_selector: None,
831 };
832
833 let err = resolve_projection(&config, None).unwrap_err();
834 assert!(
835 err.to_string()
836 .contains("Missing region metadata while resolving 'projection_names'")
837 );
838 }
839
840 #[test]
841 fn test_resolve_projection_conflict_fields() {
842 let config = ScanConfig {
843 projection: Some(vec![0]),
844 projection_names: Some(vec!["host".to_string()]),
845 filters: None,
846 series_row_selector: None,
847 };
848
849 let err = resolve_projection(&config, None).unwrap_err();
850 let msg = err.to_string();
851 assert!(msg.contains("projection"));
852 assert!(msg.contains("projection_names"));
853 }
854
855 #[test]
856 fn test_sqlparser_parse_expr_string() {
857 let dialect = GenericDialect {};
858 let mut parser = Parser::new(&dialect)
859 .try_with_sql("host = 'web-1' AND cpu > 80")
860 .unwrap();
861
862 let expr = parser.parse_expr().unwrap();
863
864 match expr {
865 Expr::BinaryOp { op, .. } => assert_eq!(op, BinaryOperator::And),
866 other => panic!("expected BinaryOp, got: {other:?}"),
867 }
868 }
869
870 #[test]
871 fn test_resolve_filters_uint32_type_conversion() {
872 use api::v1::SemanticType;
873
874 let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 0));
875 builder
876 .push_column_metadata(ColumnMetadata {
877 column_schema: ColumnSchema::new(
878 "table_id",
879 ConcreteDataType::uint32_datatype(),
880 false,
881 ),
882 semantic_type: SemanticType::Tag,
883 column_id: 1,
884 })
885 .push_column_metadata(ColumnMetadata {
886 column_schema: ColumnSchema::new(
887 "ts",
888 ConcreteDataType::timestamp_millisecond_datatype(),
889 false,
890 ),
891 semantic_type: SemanticType::Timestamp,
892 column_id: 2,
893 })
894 .primary_key(vec![1]);
895 let metadata = builder.build().unwrap();
896
897 let config = ScanConfig {
898 projection: None,
899 projection_names: None,
900 filters: Some(vec!["table_id = 1117".to_string()]),
901 series_row_selector: None,
902 };
903
904 let exprs = resolve_filters(&config, &metadata).unwrap();
905 assert_eq!(exprs.len(), 1);
906 let expr_str = format!("{}", exprs[0]);
908 assert!(
909 expr_str.contains("UInt32(1117)"),
910 "Expected UInt32(1117) in expression, got: {expr_str}"
911 );
912 }
913
914 #[test]
915 fn test_parse_scan_config_filters() {
916 let json = r#"{"filters":["host = 'web-1'","cpu > 80"]}"#;
917 let config: ScanConfig = serde_json::from_str(json).unwrap();
918
919 assert_eq!(
920 config.filters,
921 Some(vec!["host = 'web-1'".to_string(), "cpu > 80".to_string()])
922 );
923 }
924}