1use std::path::PathBuf;
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18
19use clap::{Parser, ValueEnum};
20use colored::Colorize;
21use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema, SchemaRef};
22use futures::StreamExt;
23use mito2::cache::CacheStrategy;
24use mito2::read::range::FileRangeBuilder;
25use mito2::read::read_columns::ReadColumns;
26use mito2::sst::file::{FileHandle, FileMeta, RegionFileId};
27use mito2::sst::file_purger::NoopFilePurger;
28use mito2::sst::location::sst_file_path;
29use mito2::sst::parquet::metadata::MetadataLoader;
30use mito2::sst::parquet::push_decoder::{
31 SstParquetRangeFetcher, build_sst_parquet_record_batch_stream,
32};
33use mito2::sst::parquet::reader::{MetadataCacheMetrics, ParquetReaderBuilder, ReaderMetrics};
34use mito2::sst::{FlatSchemaOptions, to_flat_sst_arrow_schema};
35use parquet::arrow::ProjectionMask;
36use parquet::arrow::arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions};
37use serde::Deserialize;
38use smallvec::SmallVec;
39use snafu::ResultExt;
40use store_api::metadata::{RegionMetadata, RegionMetadataRef};
41use store_api::region_request::PathType;
42use store_api::storage::consts::{PRIMARY_KEY_COLUMN_NAME, is_internal_column};
43use store_api::storage::{ColumnId, FileId, RegionId};
44
45use crate::datanode::objbench::{build_object_store, extract_region_metadata, parse_config};
46use crate::error;
47
48const DEFAULT_READ_BATCH_SIZE: usize = 8 * 1024;
49
50#[derive(Debug, Parser)]
52pub struct ParquetbenchCommand {
53 #[clap(long, value_name = "FILE")]
55 config: PathBuf,
56
57 #[clap(long)]
59 region_id: String,
60
61 #[clap(long)]
63 table_dir: String,
64
65 #[clap(long)]
67 file_id: String,
68
69 #[clap(long, value_name = "FILE")]
71 scan_config: Option<PathBuf>,
72
73 #[clap(long, default_value = "1")]
75 iterations: usize,
76
77 #[clap(long, default_value_t = DEFAULT_READ_BATCH_SIZE, value_parser = parse_batch_size)]
79 batch_size: usize,
80
81 #[clap(long, default_value = "bare")]
83 path_type: String,
84
85 #[clap(short, long, default_value_t = false)]
87 verbose: bool,
88
89 #[clap(long, value_name = "FILE")]
91 pprof_file: Option<PathBuf>,
92
93 #[clap(long, default_value_t = false)]
95 pprof_after_warmup: bool,
96
97 #[clap(long, default_value_t = false)]
100 pk_as_binary: bool,
101
102 #[clap(long, value_enum, default_value = "direct")]
104 reader: ReaderMode,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
108enum ReaderMode {
109 Direct,
111 FlatPrune,
113}
114
115#[derive(Debug, Deserialize, Default)]
116struct ParquetScanConfig {
117 projection_names: Option<Vec<String>>,
118 row_groups: Option<Vec<usize>>,
119}
120
121#[derive(Debug, Default, Clone)]
122struct IterationStats {
123 rows: usize,
124 record_batches: usize,
125 columns: usize,
127 schema: Option<SchemaRef>,
129 elapsed: Duration,
130}
131
132impl ParquetbenchCommand {
133 pub async fn run(&self) -> error::Result<()> {
134 if self.verbose {
135 common_telemetry::init_default_ut_logging();
136 }
137
138 println!("{}", "Starting parquetbench...".cyan().bold());
139
140 if self.iterations <= 1 && self.pprof_after_warmup && self.pprof_file.is_some() {
141 return error::IllegalConfigSnafu {
142 msg: "pprof-after-warmup requires at least 2 iterations (1 warmup + 1 profiled)"
143 .to_string(),
144 }
145 .fail();
146 }
147
148 let region_id = parse_region_id(&self.region_id)?;
149 let path_type = parse_path_type(&self.path_type)?;
150 let file_id = FileId::parse_str(&self.file_id).map_err(|e| {
151 error::IllegalConfigSnafu {
152 msg: format!("invalid file_id '{}': {}", self.file_id, e),
153 }
154 .build()
155 })?;
156 let region_file_id = RegionFileId::new(region_id, file_id);
157 let file_path = sst_file_path(&self.table_dir, region_file_id, path_type);
158
159 let (store_cfg, _mito_config, _wal_config) = parse_config(&self.config)?;
160 let object_store = build_object_store(&store_cfg).await?;
161
162 let file_size = object_store
163 .stat(&file_path)
164 .await
165 .map_err(|e| {
166 error::IllegalConfigSnafu {
167 msg: format!("stat failed for {}: {}", file_path, e),
168 }
169 .build()
170 })?
171 .content_length();
172 let mut metadata_metrics = MetadataCacheMetrics::default();
173 let parquet_meta = MetadataLoader::new(object_store.clone(), &file_path, file_size)
174 .load(&mut metadata_metrics)
175 .await
176 .map_err(|e| {
177 error::IllegalConfigSnafu {
178 msg: format!("read parquet metadata failed for {}: {:?}", file_path, e),
179 }
180 .build()
181 })?;
182 let region_meta = extract_region_metadata(&file_path, &parquet_meta)?;
183 let scan_config = self.load_scan_config().await?;
184 let projection = if self.reader == ReaderMode::Direct {
185 resolve_projection_names(&scan_config, ®ion_meta)?
186 } else {
187 None
188 };
189 let projection_column_ids = if self.reader == ReaderMode::FlatPrune {
190 resolve_projection_column_ids(&scan_config, ®ion_meta)?
191 } else {
192 None
193 };
194 let row_groups = resolve_row_groups(&scan_config, parquet_meta.num_row_groups())?;
195 let read_all_row_groups = scan_config.row_groups.is_none();
196 let scanned_bytes = scanned_row_group_bytes(&parquet_meta, &row_groups);
197 let projected_columns = projection_names_display(&scan_config);
198 let row_groups_display = row_groups_display(&row_groups, parquet_meta.num_row_groups());
199 let mut sst_schema = to_flat_sst_arrow_schema(
200 ®ion_meta,
201 &FlatSchemaOptions::from_encoding(region_meta.primary_key_encoding),
202 );
203 if self.pk_as_binary {
204 sst_schema = override_pk_to_binary(&sst_schema);
205 }
206
207 println!(
208 "{} Reader: {}",
209 "✓".green(),
210 match self.reader {
211 ReaderMode::Direct => "direct",
212 ReaderMode::FlatPrune => "flat-prune",
213 }
214 .cyan()
215 );
216 println!(
217 "{} Region ID: {} (u64: {})",
218 "✓".green(),
219 self.region_id,
220 region_id.as_u64()
221 );
222 println!("{} File path: {}", "✓".green(), file_path.cyan());
223 println!(
224 "{} Columns: {}",
225 "✓".green(),
226 projected_columns.as_deref().unwrap_or("all columns").cyan()
227 );
228 println!("{} Row groups: {}", "✓".green(), row_groups_display.cyan());
229 if !read_all_row_groups {
230 println!(
231 "{} Scanned bytes (selected row groups): {}",
232 "✓".green(),
233 format_bytes(scanned_bytes).cyan()
234 );
235 }
236 println!(
237 "{} __primary_key type: {}",
238 "✓".green(),
239 if self.pk_as_binary {
240 "Binary"
241 } else {
242 "Dictionary(UInt32, Binary)"
243 }
244 .cyan()
245 );
246 match self.reader {
247 ReaderMode::Direct => {
248 println!("{} Batch size: {}", "✓".green(), self.batch_size);
249 }
250 ReaderMode::FlatPrune => {
251 println!(
252 "{} Batch size: {} (flat-prune internal default)",
253 "✓".green(),
254 DEFAULT_READ_BATCH_SIZE
255 );
256 println!(
257 "{} --batch-size is only used by the direct reader; ignoring {} in flat-prune mode",
258 "ℹ".blue(),
259 self.batch_size
260 );
261 }
262 }
263 println!(
264 "{} Parquet rows: {}, row groups: {}, file size: {}",
265 "✓".green(),
266 parquet_meta.file_metadata().num_rows(),
267 parquet_meta.num_row_groups(),
268 format_bytes(file_size)
269 );
270 println!(
271 "{} Metadata reads: {}, bytes: {}",
272 "✓".green(),
273 metadata_metrics.num_reads,
274 format_bytes(metadata_metrics.bytes_read)
275 );
276
277 #[cfg(unix)]
278 let mut profiler_guard = if self.pprof_file.is_some() && !self.pprof_after_warmup {
279 println!("{} Starting profiling...", "⚡".yellow());
280 Some(
281 pprof::ProfilerGuardBuilder::default()
282 .frequency(99)
283 .blocklist(&["libc", "libgcc", "pthread", "vdso"])
284 .build()
285 .map_err(|e| {
286 error::IllegalConfigSnafu {
287 msg: format!("Failed to start profiler: {e}"),
288 }
289 .build()
290 })?,
291 )
292 } else {
293 None
294 };
295
296 #[cfg(not(unix))]
297 if self.pprof_file.is_some() {
298 eprintln!(
299 "{}: Profiling is not supported on this platform",
300 "Warning".yellow()
301 );
302 }
303
304 let mut total_elapsed_all = Duration::ZERO;
305 let mut total_rows_all = 0usize;
306 let mut total_batches_all = 0usize;
307 let mut schema_printed = false;
308 let file_handle = FileHandle::new(
309 FileMeta {
310 region_id,
311 file_id,
312 time_range: Default::default(),
313 level: 0,
314 file_size,
315 max_row_group_uncompressed_size: 0,
316 available_indexes: Default::default(),
317 indexes: Default::default(),
318 index_file_size: 0,
319 index_version: 0,
320 num_rows: parquet_meta.file_metadata().num_rows() as u64,
321 num_row_groups: parquet_meta.num_row_groups() as u64,
322 sequence: None,
323 partition_expr: None,
324 num_series: 0,
325 primary_key_min: None,
326 primary_key_max: None,
327 },
328 Arc::new(NoopFilePurger),
329 );
330
331 for iteration in 0..self.iterations {
332 let stats = match self.reader {
333 ReaderMode::Direct => {
334 run_direct_iteration(
335 object_store.clone(),
336 file_path.clone(),
337 region_file_id,
338 parquet_meta.clone(),
339 projection.clone(),
340 row_groups.clone(),
341 sst_schema.clone(),
342 self.batch_size,
343 )
344 .await?
345 }
346 ReaderMode::FlatPrune => {
347 run_flat_prune_iteration(
348 object_store.clone(),
349 self.table_dir.clone(),
350 path_type,
351 file_handle.clone(),
352 region_meta.clone(),
353 projection_column_ids.clone(),
354 row_groups.clone(),
355 read_all_row_groups,
356 )
357 .await?
358 }
359 };
360
361 total_elapsed_all += stats.elapsed;
362 total_rows_all += stats.rows;
363 total_batches_all += stats.record_batches;
364
365 if !schema_printed && let Some(schema) = &stats.schema {
366 println!(
367 "{} Output schema ({} columns):",
368 "✓".green(),
369 schema.fields().len()
370 );
371 for field in schema.fields() {
372 println!(" - {}: {}", field.name().cyan(), field.data_type());
373 }
374 schema_printed = true;
375 }
376
377 println!(
378 " Iteration {}: {} rows, {} columns, {} record batches in {:?} ({}/s, {}/s)",
379 iteration + 1,
380 stats.rows,
381 stats.columns,
382 stats.record_batches,
383 stats.elapsed,
384 format_rate(stats.rows as f64 / stats.elapsed.as_secs_f64()),
385 format_bytes_per_sec(scanned_bytes as f64 / stats.elapsed.as_secs_f64()),
386 );
387
388 #[cfg(unix)]
389 if iteration == 0 && self.pprof_after_warmup && self.pprof_file.is_some() {
390 println!("{} Starting profiling after warmup...", "⚡".yellow());
391 profiler_guard = Some(
392 pprof::ProfilerGuardBuilder::default()
393 .frequency(99)
394 .blocklist(&["libc", "libgcc", "pthread", "vdso"])
395 .build()
396 .map_err(|e| {
397 error::IllegalConfigSnafu {
398 msg: format!("Failed to start profiler: {e}"),
399 }
400 .build()
401 })?,
402 );
403 }
404 }
405
406 #[cfg(unix)]
407 if let (Some(guard), Some(pprof_file)) = (profiler_guard, &self.pprof_file) {
408 println!("{} Generating flamegraph...", "🔥".yellow());
409 match guard.report().build() {
410 Ok(report) => {
411 let mut flamegraph_data = Vec::new();
412 if let Err(e) = report.flamegraph(&mut flamegraph_data) {
413 println!("{}: Failed to generate flamegraph: {}", "Error".red(), e);
414 } else if let Err(e) = std::fs::write(pprof_file, flamegraph_data) {
415 println!(
416 "{}: Failed to write flamegraph to {}: {}",
417 "Error".red(),
418 pprof_file.display(),
419 e
420 );
421 } else {
422 println!(
423 "{} Flamegraph saved to {}",
424 "✓".green(),
425 pprof_file.display().to_string().cyan()
426 );
427 }
428 }
429 Err(e) => {
430 println!("{}: Failed to generate pprof report: {}", "Error".red(), e);
431 }
432 }
433 }
434
435 if self.iterations > 1 {
436 let avg_elapsed = total_elapsed_all / self.iterations as u32;
437 let avg_rows = total_rows_all / self.iterations;
438 let avg_batches = total_batches_all / self.iterations;
439 println!(
440 "\n{} Average: {} rows, {} record batches in {:?} over {} iterations",
441 "ℹ".blue(),
442 avg_rows,
443 avg_batches,
444 avg_elapsed,
445 self.iterations
446 );
447 }
448
449 println!("\n{}", "Benchmark completed!".green().bold());
450 Ok(())
451 }
452
453 async fn load_scan_config(&self) -> error::Result<ParquetScanConfig> {
454 if let Some(path) = &self.scan_config {
455 let content = tokio::fs::read_to_string(path)
456 .await
457 .context(error::FileIoSnafu)?;
458 serde_json::from_str::<ParquetScanConfig>(&content).context(error::SerdeJsonSnafu)
459 } else {
460 Ok(ParquetScanConfig::default())
461 }
462 }
463}
464
465#[allow(clippy::too_many_arguments)]
466async fn run_direct_iteration(
467 object_store: object_store::ObjectStore,
468 file_path: String,
469 region_file_id: RegionFileId,
470 parquet_meta: parquet::file::metadata::ParquetMetaData,
471 projection: Option<Vec<usize>>,
472 row_groups: Vec<usize>,
473 sst_schema: SchemaRef,
474 batch_size: usize,
475) -> error::Result<IterationStats> {
476 let parquet_meta = Arc::new(parquet_meta);
477 let arrow_metadata = ArrowReaderMetadata::try_new(
478 parquet_meta.clone(),
479 ArrowReaderOptions::new().with_schema(sst_schema),
480 )
481 .map_err(|e| {
482 error::IllegalConfigSnafu {
483 msg: format!(
484 "Failed to build parquet arrow metadata for {}: {}",
485 file_path, e
486 ),
487 }
488 .build()
489 })?;
490 let projection_mask = match projection.as_ref() {
491 Some(projection) => {
492 ProjectionMask::roots(arrow_metadata.parquet_schema(), projection.iter().copied())
493 }
494 None => ProjectionMask::all(),
495 };
496 let start = Instant::now();
497 let mut stats = IterationStats::default();
498 for row_group_idx in row_groups {
499 let fetcher = SstParquetRangeFetcher::new(
500 region_file_id,
501 file_path.clone(),
502 object_store.clone(),
503 CacheStrategy::Disabled,
504 row_group_idx,
505 None,
506 );
507 let mut stream = build_sst_parquet_record_batch_stream(
508 arrow_metadata.clone(),
509 row_group_idx,
510 None,
511 projection_mask.clone(),
512 fetcher,
513 file_path.clone(),
514 batch_size,
515 )
516 .map_err(|e| {
517 error::IllegalConfigSnafu {
518 msg: format!(
519 "Failed to build parquet record batch stream for {}: {e:?}",
520 file_path
521 ),
522 }
523 .build()
524 })?;
525 while let Some(batch) = stream.next().await.transpose().map_err(|e| {
526 error::IllegalConfigSnafu {
527 msg: format!("Failed to scan parquet file {}: {e:?}", file_path),
528 }
529 .build()
530 })? {
531 stats.rows += batch.num_rows();
532 stats.record_batches += 1;
533 stats.columns = batch.num_columns();
534 if stats.schema.is_none() {
535 stats.schema = Some(batch.schema());
536 }
537 }
538 }
539 stats.elapsed = start.elapsed();
540 Ok(stats)
541}
542
543#[allow(clippy::too_many_arguments)]
544async fn run_flat_prune_iteration(
545 object_store: object_store::ObjectStore,
546 table_dir: String,
547 path_type: PathType,
548 file_handle: FileHandle,
549 region_meta: RegionMetadataRef,
550 projection: Option<Vec<ColumnId>>,
551 row_groups: Vec<usize>,
552 read_all_row_groups: bool,
553) -> error::Result<IterationStats> {
554 let reader_builder = ParquetReaderBuilder::new(table_dir, path_type, file_handle, object_store)
555 .expected_metadata(Some(region_meta))
556 .cache(CacheStrategy::Disabled)
557 .projection(projection.map(ReadColumns::from_deduped_column_ids));
558 let mut reader_metrics = ReaderMetrics::default();
559 let start = Instant::now();
560 let mut stats = IterationStats::default();
561 let Some((context, selection)) = reader_builder
562 .build_reader_input(&mut reader_metrics)
563 .await
564 .map_err(|e| {
565 error::IllegalConfigSnafu {
566 msg: format!("build flat prune reader input failed: {e:?}"),
567 }
568 .build()
569 })?
570 else {
571 stats.elapsed = start.elapsed();
572 return Ok(stats);
573 };
574
575 let range_builder = FileRangeBuilder::new(Arc::new(context), selection);
576 let mut ranges = SmallVec::new();
577 if read_all_row_groups {
578 range_builder.build_ranges(-1, &mut ranges);
579 } else {
580 for row_group_idx in row_groups {
581 range_builder.build_ranges(row_group_idx as i64, &mut ranges);
582 }
583 }
584
585 for range in ranges {
586 let Some(mut reader) = range.flat_reader(None, None).await.map_err(|e| {
587 error::IllegalConfigSnafu {
588 msg: format!("build flat prune reader failed: {e:?}"),
589 }
590 .build()
591 })?
592 else {
593 continue;
594 };
595 while let Some(batch) = reader.next_batch().await.map_err(|e| {
596 error::IllegalConfigSnafu {
597 msg: format!("scan flat prune reader failed: {e:?}"),
598 }
599 .build()
600 })? {
601 stats.rows += batch.num_rows();
602 stats.record_batches += 1;
603 stats.columns = batch.num_columns();
604 if stats.schema.is_none() {
605 stats.schema = Some(batch.schema());
606 }
607 }
608 }
609
610 stats.elapsed = start.elapsed();
611 Ok(stats)
612}
613
614fn resolve_projection_names(
615 scan_config: &ParquetScanConfig,
616 metadata: &RegionMetadata,
617) -> error::Result<Option<Vec<usize>>> {
618 let Some(projection_names) = &scan_config.projection_names else {
619 return Ok(None);
620 };
621
622 let sst_schema = to_flat_sst_arrow_schema(
623 metadata,
624 &FlatSchemaOptions::from_encoding(metadata.primary_key_encoding),
625 );
626 let available_columns = sst_schema
627 .fields()
628 .iter()
629 .map(|field| field.name().as_str())
630 .collect::<Vec<_>>()
631 .join(", ");
632 let projection = projection_names
633 .iter()
634 .map(|name| {
635 sst_schema
636 .column_with_name(name)
637 .map(|x| x.0)
638 .ok_or_else(|| {
639 error::IllegalConfigSnafu {
640 msg: format!(
641 "Unknown column '{}' in projection_names, available columns: [{}]",
642 name, available_columns
643 ),
644 }
645 .build()
646 })
647 })
648 .collect::<error::Result<Vec<_>>>()?;
649 Ok(Some(projection))
650}
651
652fn resolve_projection_column_ids(
653 scan_config: &ParquetScanConfig,
654 metadata: &RegionMetadata,
655) -> error::Result<Option<Vec<ColumnId>>> {
656 let Some(projection_names) = &scan_config.projection_names else {
657 return Ok(None);
658 };
659
660 let available_columns = metadata
661 .column_metadatas
662 .iter()
663 .map(|column| column.column_schema.name.as_str())
664 .collect::<Vec<_>>()
665 .join(", ");
666 let projection = projection_names
667 .iter()
668 .filter_map(|name| {
669 if is_internal_column(name) {
670 return None;
671 }
672 Some(
673 metadata
674 .column_metadatas
675 .iter()
676 .find(|column| column.column_schema.name == *name)
677 .map(|column| column.column_id)
678 .ok_or_else(|| {
679 error::IllegalConfigSnafu {
680 msg: format!(
681 "Unknown column '{}' in projection_names, available columns: [{}]",
682 name, available_columns
683 ),
684 }
685 .build()
686 }),
687 )
688 })
689 .collect::<error::Result<Vec<_>>>()?;
690 Ok(Some(projection))
691}
692
693fn resolve_row_groups(
694 scan_config: &ParquetScanConfig,
695 num_row_groups: usize,
696) -> error::Result<Vec<usize>> {
697 match &scan_config.row_groups {
698 Some(row_groups) => {
699 for row_group_idx in row_groups {
700 if *row_group_idx >= num_row_groups {
701 return Err(error::IllegalConfigSnafu {
702 msg: format!(
703 "Invalid row group {} in row_groups, parquet file has row groups [0, {})",
704 row_group_idx, num_row_groups
705 ),
706 }
707 .build());
708 }
709 }
710 Ok(row_groups.clone())
711 }
712 None => Ok((0..num_row_groups).collect()),
713 }
714}
715
716fn scanned_row_group_bytes(
719 parquet_meta: &parquet::file::metadata::ParquetMetaData,
720 row_groups: &[usize],
721) -> u64 {
722 row_groups
723 .iter()
724 .map(|&idx| parquet_meta.row_group(idx).compressed_size() as u64)
725 .sum()
726}
727
728fn projection_names_display(scan_config: &ParquetScanConfig) -> Option<String> {
729 scan_config
730 .projection_names
731 .as_ref()
732 .map(|cols| cols.join(", "))
733}
734
735fn row_groups_display(row_groups: &[usize], total_row_groups: usize) -> String {
736 if row_groups.len() == total_row_groups {
737 "all row groups".to_string()
738 } else {
739 row_groups
740 .iter()
741 .map(|idx| idx.to_string())
742 .collect::<Vec<_>>()
743 .join(", ")
744 }
745}
746
747fn override_pk_to_binary(schema: &SchemaRef) -> SchemaRef {
748 let new_fields: Vec<_> = schema
749 .fields()
750 .iter()
751 .map(|f| {
752 if f.name() == PRIMARY_KEY_COLUMN_NAME {
753 Arc::new(Field::new(
754 PRIMARY_KEY_COLUMN_NAME,
755 ArrowDataType::Binary,
756 f.is_nullable(),
757 ))
758 } else {
759 f.clone()
760 }
761 })
762 .collect();
763 Arc::new(Schema::new(new_fields))
764}
765
766fn parse_batch_size(s: &str) -> Result<usize, String> {
767 let batch_size = s
768 .parse::<usize>()
769 .map_err(|e| format!("invalid batch size '{s}': {e}"))?;
770 if batch_size == 0 {
771 return Err("batch size must be greater than 0".to_string());
772 }
773 Ok(batch_size)
774}
775
776fn parse_region_id(s: &str) -> error::Result<RegionId> {
777 if s.contains(':') {
778 let parts: Vec<&str> = s.splitn(2, ':').collect();
779 let table_id: u32 = parts[0].parse().map_err(|e| {
780 error::IllegalConfigSnafu {
781 msg: format!("invalid table_id in region_id '{}': {}", s, e),
782 }
783 .build()
784 })?;
785 let region_num: u32 = parts[1].parse().map_err(|e| {
786 error::IllegalConfigSnafu {
787 msg: format!("invalid region_num in region_id '{}': {}", s, e),
788 }
789 .build()
790 })?;
791 Ok(RegionId::new(table_id, region_num))
792 } else {
793 let id: u64 = s.parse().map_err(|e| {
794 error::IllegalConfigSnafu {
795 msg: format!("invalid region_id '{}': {}", s, e),
796 }
797 .build()
798 })?;
799 Ok(RegionId::from_u64(id))
800 }
801}
802
803fn parse_path_type(s: &str) -> error::Result<PathType> {
804 match s.to_lowercase().as_str() {
805 "bare" => Ok(PathType::Bare),
806 "data" => Ok(PathType::Data),
807 "metadata" => Ok(PathType::Metadata),
808 _ => Err(error::IllegalConfigSnafu {
809 msg: format!("invalid path_type '{}', expected: bare, data, metadata", s),
810 }
811 .build()),
812 }
813}
814
815fn format_bytes(bytes: u64) -> String {
816 const KIB: u64 = 1024;
817 const MIB: u64 = 1024 * KIB;
818 const GIB: u64 = 1024 * MIB;
819 if bytes >= GIB {
820 format!("{:.2} GiB", bytes as f64 / GIB as f64)
821 } else if bytes >= MIB {
822 format!("{:.2} MiB", bytes as f64 / MIB as f64)
823 } else if bytes >= KIB {
824 format!("{:.2} KiB", bytes as f64 / KIB as f64)
825 } else {
826 format!("{} B", bytes)
827 }
828}
829
830fn format_rate(rate: f64) -> String {
831 if !rate.is_finite() {
832 return "inf rows".to_string();
833 }
834 format!("{rate:.2} rows")
835}
836
837fn format_bytes_per_sec(bytes_per_sec: f64) -> String {
838 if !bytes_per_sec.is_finite() {
839 return "inf B/s".to_string();
840 }
841 format!("{}/s", format_bytes(bytes_per_sec as u64))
842}
843
844#[cfg(test)]
845mod tests {
846 use api::v1::SemanticType;
847 use datatypes::prelude::ConcreteDataType;
848 use serde_json::json;
849 use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
850 use store_api::storage::ColumnSchema;
851
852 use super::*;
853
854 fn new_test_metadata() -> RegionMetadata {
855 let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 0));
856 builder
857 .push_column_metadata(ColumnMetadata {
858 column_schema: ColumnSchema::new(
859 "host",
860 ConcreteDataType::string_datatype(),
861 false,
862 ),
863 semantic_type: SemanticType::Tag,
864 column_id: 1,
865 })
866 .push_column_metadata(ColumnMetadata {
867 column_schema: ColumnSchema::new("cpu", ConcreteDataType::float64_datatype(), true),
868 semantic_type: SemanticType::Field,
869 column_id: 2,
870 })
871 .push_column_metadata(ColumnMetadata {
872 column_schema: ColumnSchema::new(
873 "ts",
874 ConcreteDataType::timestamp_millisecond_datatype(),
875 false,
876 ),
877 semantic_type: SemanticType::Timestamp,
878 column_id: 3,
879 })
880 .primary_key(vec![1]);
881 builder.build().unwrap()
882 }
883
884 #[test]
885 fn test_parse_region_id() {
886 assert_eq!(parse_region_id("1024:7").unwrap(), RegionId::new(1024, 7));
887 assert_eq!(
888 parse_region_id(&RegionId::new(1, 2).as_u64().to_string()).unwrap(),
889 RegionId::new(1, 2)
890 );
891 }
892
893 #[test]
894 fn test_parse_path_type() {
895 assert_eq!(parse_path_type("bare").unwrap(), PathType::Bare);
896 assert_eq!(parse_path_type("data").unwrap(), PathType::Data);
897 assert_eq!(parse_path_type("metadata").unwrap(), PathType::Metadata);
898 }
899
900 #[test]
901 fn test_parse_scan_config_projection_names() {
902 let config: ParquetScanConfig =
903 serde_json::from_value(json!({ "projection_names": ["host", "ts"] })).unwrap();
904 assert_eq!(
905 config.projection_names,
906 Some(vec!["host".to_string(), "ts".to_string()])
907 );
908 }
909
910 #[test]
911 fn test_parse_scan_config_row_groups() {
912 let config: ParquetScanConfig =
913 serde_json::from_value(json!({ "row_groups": [0, 2, 4] })).unwrap();
914 assert_eq!(config.row_groups, Some(vec![0, 2, 4]));
915 }
916
917 #[test]
918 fn test_resolve_projection_names() {
919 let metadata = new_test_metadata();
920 let projection = resolve_projection_names(
921 &ParquetScanConfig {
922 projection_names: Some(vec!["cpu".to_string(), "host".to_string()]),
923 row_groups: None,
924 },
925 &metadata,
926 )
927 .unwrap();
928 assert_eq!(projection, Some(vec![1, 0]));
929 }
930
931 #[test]
932 fn test_resolve_projection_column_ids() {
933 let metadata = new_test_metadata();
934 let projection = resolve_projection_column_ids(
935 &ParquetScanConfig {
936 projection_names: Some(vec!["cpu".to_string(), "host".to_string()]),
937 row_groups: None,
938 },
939 &metadata,
940 )
941 .unwrap();
942 assert_eq!(projection, Some(vec![2, 1]));
943 }
944
945 #[test]
946 fn test_resolve_projection_column_ids_ignores_internal_columns() {
947 let metadata = new_test_metadata();
948 let projection = resolve_projection_column_ids(
949 &ParquetScanConfig {
950 projection_names: Some(vec![
951 "cpu".to_string(),
952 "__primary_key".to_string(),
953 "__sequence".to_string(),
954 "__op_type".to_string(),
955 ]),
956 row_groups: None,
957 },
958 &metadata,
959 )
960 .unwrap();
961 assert_eq!(projection, Some(vec![2]));
962 }
963
964 #[test]
965 fn test_resolve_projection_names_unknown() {
966 let metadata = new_test_metadata();
967 let err = resolve_projection_names(
968 &ParquetScanConfig {
969 projection_names: Some(vec!["memory".to_string()]),
970 row_groups: None,
971 },
972 &metadata,
973 )
974 .unwrap_err();
975 let msg = err.to_string();
976 assert!(msg.contains("projection_names"));
977 assert!(msg.contains("host"));
978 assert!(msg.contains("cpu"));
979 assert!(msg.contains("ts"));
980 }
981
982 #[test]
983 fn test_resolve_row_groups_all() {
984 assert_eq!(
985 resolve_row_groups(&ParquetScanConfig::default(), 3).unwrap(),
986 vec![0, 1, 2]
987 );
988 }
989
990 #[test]
991 fn test_resolve_row_groups_subset() {
992 let config = ParquetScanConfig {
993 projection_names: None,
994 row_groups: Some(vec![2, 0]),
995 };
996 assert_eq!(resolve_row_groups(&config, 4).unwrap(), vec![2, 0]);
997 }
998
999 #[test]
1000 fn test_resolve_row_groups_invalid() {
1001 let config = ParquetScanConfig {
1002 projection_names: None,
1003 row_groups: Some(vec![3]),
1004 };
1005 let err = resolve_row_groups(&config, 3).unwrap_err();
1006 assert!(err.to_string().contains("Invalid row group 3"));
1007 }
1008
1009 #[test]
1010 fn test_sst_file_path_resolution() {
1011 let file_id = FileId::parse_str("00020380-009c-426d-953e-b4e34c15af34").unwrap();
1012 let region_file_id = RegionFileId::new(RegionId::new(1024, 0), file_id);
1013 assert_eq!(
1014 sst_file_path("data/greptime/public/1024", region_file_id, PathType::Bare),
1015 "data/greptime/public/1024/1024_0000000000/00020380-009c-426d-953e-b4e34c15af34.parquet"
1016 );
1017 }
1018}