1use std::borrow::Cow;
16use std::collections::{HashMap, HashSet};
17use std::sync::Arc;
18use std::task::Poll;
19use std::time::Instant;
20
21use common_telemetry::warn;
22use datafusion::arrow::array::{Array, ArrayRef, AsArray};
23use datafusion::arrow::compute::{SortOptions, concat_batches};
24use datafusion::arrow::datatypes::{DataType, Float64Type, SchemaRef};
25use datafusion::arrow::record_batch::RecordBatch;
26use datafusion::common::stats::Precision;
27use datafusion::common::tree_node::TreeNodeRecursion;
28use datafusion::common::{DFSchema, DFSchemaRef, Statistics};
29use datafusion::error::{DataFusionError, Result as DataFusionResult};
30use datafusion::execution::TaskContext;
31use datafusion::logical_expr::{LogicalPlan, UserDefinedLogicalNodeCore};
32use datafusion::physical_expr::{
33 EquivalenceProperties, LexRequirement, OrderingRequirements, PhysicalSortRequirement,
34};
35use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
36use datafusion::physical_plan::expressions::{Column as PhyColumn, TryCastExpr as PhyTryCast};
37use datafusion::physical_plan::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet};
38use datafusion::physical_plan::{
39 DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, ExecutionPlanProperties,
40 InputDistributionRequirements, Partitioning, PhysicalExpr, PlanProperties, RecordBatchStream,
41 SendableRecordBatchStream, StatisticsArgs,
42};
43use datafusion::prelude::{Column, Expr};
44use datafusion_expr::{EmptyRelation, ident};
45use datatypes::arrow_array::string_array_value_at_index;
46use datatypes::prelude::{ConcreteDataType, DataType as GtDataType};
47use datatypes::value::{OrderedF64, Value, ValueRef};
48use datatypes::vectors::{Helper, MutableVector, VectorRef};
49use futures::{Stream, StreamExt, ready};
50use greptime_proto::substrait_extension as pb;
51use prost::Message;
52use snafu::ResultExt;
53
54use crate::error::{DeserializeSnafu, Result};
55use crate::extension_plan::{resolve_column_name, serialize_column_index};
56
57#[derive(Debug, PartialEq, Hash, Eq)]
74pub struct HistogramFold {
75 le_column: String,
79 ts_column: String,
80 input: LogicalPlan,
81 field_column: String,
82 histogram_column: Option<String>,
84 operation: HistogramFoldOperation,
85 output_schema: DFSchemaRef,
86 unfix: Option<UnfixIndices>,
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd)]
90pub enum HistogramFoldOperation {
91 Quantile(OrderedF64),
92 Fraction {
93 lower: OrderedF64,
94 upper: OrderedF64,
95 },
96}
97
98impl HistogramFoldOperation {
99 pub const fn function_name(self) -> &'static str {
100 match self {
101 Self::Quantile(_) => "histogram_quantile",
102 Self::Fraction { .. } => "histogram_fraction",
103 }
104 }
105}
106
107impl std::fmt::Display for HistogramFoldOperation {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 Self::Quantile(quantile) => write!(f, "quantile={quantile}"),
111 Self::Fraction { lower, upper } => {
112 write!(f, "fraction=[{lower}, {upper}]")
113 }
114 }
115 }
116}
117
118#[derive(Debug, PartialEq, Eq, Hash, PartialOrd)]
119struct UnfixIndices {
120 pub le_column_idx: u64,
121 pub ts_column_idx: u64,
122 pub field_column_idx: u64,
123}
124
125impl UserDefinedLogicalNodeCore for HistogramFold {
126 fn name(&self) -> &str {
127 Self::name()
128 }
129
130 fn inputs(&self) -> Vec<&LogicalPlan> {
131 vec![&self.input]
132 }
133
134 fn schema(&self) -> &DFSchemaRef {
135 &self.output_schema
136 }
137
138 fn expressions(&self) -> Vec<Expr> {
139 if self.unfix.is_some() {
140 return vec![];
141 }
142
143 let mut exprs = vec![
144 ident(&self.le_column),
145 ident(&self.ts_column),
146 ident(&self.field_column),
147 ];
148 exprs.extend(self.input.schema().fields().iter().filter_map(|f| {
149 let name = f.name();
150 if name != &self.le_column && name != &self.ts_column && name != &self.field_column {
151 Some(ident(name))
152 } else {
153 None
154 }
155 }));
156 exprs
157 }
158
159 fn necessary_children_exprs(&self, output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
160 if self.unfix.is_some() {
161 return None;
162 }
163
164 let input_schema = self.input.schema();
165 let le_column_index = input_schema.index_of_column_by_name(None, &self.le_column)?;
166
167 if output_columns.is_empty() {
168 let indices = (0..input_schema.fields().len()).collect::<Vec<_>>();
169 return Some(vec![indices]);
170 }
171
172 if let Some(histogram_column) = &self.histogram_column {
173 let mut necessary_indices = output_columns.to_vec();
174 for column in [
175 &self.le_column,
176 &self.ts_column,
177 &self.field_column,
178 histogram_column,
179 ] {
180 necessary_indices.push(input_schema.index_of_column_by_name(None, column)?);
181 }
182 necessary_indices.sort_unstable();
183 necessary_indices.dedup();
184 return Some(vec![necessary_indices]);
185 }
186
187 let mut necessary_indices = output_columns
188 .iter()
189 .map(|&output_column| {
190 if output_column < le_column_index {
191 output_column
192 } else {
193 output_column + 1
194 }
195 })
196 .collect::<Vec<_>>();
197 necessary_indices.push(le_column_index);
198 necessary_indices.sort_unstable();
199 necessary_indices.dedup();
200 Some(vec![necessary_indices])
201 }
202
203 fn fmt_for_explain(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 write!(
205 f,
206 "HistogramFold: le={}, field={}",
207 self.le_column, self.field_column
208 )?;
209 if let Some(histogram) = &self.histogram_column {
210 write!(f, ", histogram={histogram}")?;
211 }
212 write!(f, ", {}", self.operation)
213 }
214
215 fn with_exprs_and_inputs(
216 &self,
217 _exprs: Vec<Expr>,
218 inputs: Vec<LogicalPlan>,
219 ) -> DataFusionResult<Self> {
220 if inputs.is_empty() {
221 return Err(DataFusionError::Internal(
222 "HistogramFold must have at least one input".to_string(),
223 ));
224 }
225
226 let input: LogicalPlan = inputs.into_iter().next().unwrap();
227 let input_schema = input.schema();
228
229 if let Some(unfix) = &self.unfix {
230 let le_column =
231 resolve_column_name(unfix.le_column_idx, input_schema, "HistogramFold", "le")?;
232 let ts_column =
233 resolve_column_name(unfix.ts_column_idx, input_schema, "HistogramFold", "ts")?;
234 let field_column = resolve_column_name(
235 unfix.field_column_idx,
236 input_schema,
237 "HistogramFold",
238 "field",
239 )?;
240
241 let output_schema = Self::convert_schema(input_schema, &le_column)?;
242
243 Ok(Self {
244 le_column,
245 ts_column,
246 input,
247 field_column,
248 histogram_column: None,
249 operation: self.operation,
250 output_schema,
251 unfix: None,
252 })
253 } else {
254 Ok(Self {
255 le_column: self.le_column.clone(),
256 ts_column: self.ts_column.clone(),
257 input,
258 field_column: self.field_column.clone(),
259 histogram_column: self.histogram_column.clone(),
260 operation: self.operation,
261 output_schema: self.output_schema.clone(),
262 unfix: None,
263 })
264 }
265 }
266}
267
268impl HistogramFold {
269 pub fn new(
270 le_column: String,
271 field_column: String,
272 ts_column: String,
273 quantile: f64,
274 input: LogicalPlan,
275 ) -> DataFusionResult<Self> {
276 Self::new_with_operation(
277 le_column,
278 field_column,
279 ts_column,
280 HistogramFoldOperation::Quantile(quantile.into()),
281 None,
282 input,
283 )
284 }
285
286 pub fn new_with_operation(
287 le_column: String,
288 field_column: String,
289 ts_column: String,
290 operation: HistogramFoldOperation,
291 histogram_column: Option<String>,
292 input: LogicalPlan,
293 ) -> DataFusionResult<Self> {
294 let input_schema = input.schema();
295 Self::check_schema(input_schema, &le_column, &field_column, &ts_column)?;
296 if let Some(histogram_column) = &histogram_column {
297 Self::check_column(input_schema, histogram_column)?;
298 }
299 let output_schema = if histogram_column.is_some() {
300 input_schema.clone()
301 } else {
302 Self::convert_schema(input_schema, &le_column)?
303 };
304 Ok(Self {
305 le_column,
306 ts_column,
307 input,
308 field_column,
309 histogram_column,
310 operation,
311 output_schema,
312 unfix: None,
313 })
314 }
315
316 pub const fn name() -> &'static str {
317 "HistogramFold"
318 }
319
320 fn check_schema(
321 input_schema: &DFSchemaRef,
322 le_column: &str,
323 field_column: &str,
324 ts_column: &str,
325 ) -> DataFusionResult<()> {
326 Self::check_column(input_schema, le_column)?;
327 Self::check_column(input_schema, ts_column)?;
328 Self::check_column(input_schema, field_column)
329 }
330
331 fn check_column(input_schema: &DFSchemaRef, column: &str) -> DataFusionResult<()> {
332 if !input_schema.has_column_with_unqualified_name(column) {
333 return Err(DataFusionError::SchemaError(
334 Box::new(datafusion::common::SchemaError::FieldNotFound {
335 field: Box::new(Column::new(None::<String>, column)),
336 valid_fields: input_schema.columns(),
337 }),
338 Box::new(None),
339 ));
340 }
341 Ok(())
342 }
343
344 pub fn to_execution_plan(&self, exec_input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
345 let input_schema = self.input.schema();
346 let le_column_index = input_schema
348 .index_of_column_by_name(None, &self.le_column)
349 .unwrap();
350 let field_column_index = input_schema
351 .index_of_column_by_name(None, &self.field_column)
352 .unwrap();
353 let histogram_column_index = self
354 .histogram_column
355 .as_ref()
356 .map(|column| input_schema.index_of_column_by_name(None, column).unwrap());
357 let ts_column_index = input_schema
358 .index_of_column_by_name(None, &self.ts_column)
359 .unwrap();
360
361 let tag_columns = exec_input
362 .schema()
363 .fields()
364 .iter()
365 .enumerate()
366 .filter_map(|(idx, field)| {
367 if idx == le_column_index
368 || idx == field_column_index
369 || Some(idx) == histogram_column_index
370 || idx == ts_column_index
371 {
372 None
373 } else {
374 Some(Arc::new(PhyColumn::new(field.name(), idx)) as _)
375 }
376 })
377 .collect::<Vec<_>>();
378
379 let mut partition_exprs = tag_columns.clone();
380 partition_exprs.push(Arc::new(PhyColumn::new(
381 self.input.schema().field(ts_column_index).name(),
382 ts_column_index,
383 )) as _);
384
385 let output_schema: SchemaRef = self.output_schema.inner().clone();
386 let properties = Arc::new(PlanProperties::new(
387 EquivalenceProperties::new(output_schema.clone()),
388 Partitioning::UnknownPartitioning(exec_input.output_partitioning().partition_count()),
389 EmissionType::Incremental,
390 Boundedness::Bounded,
391 ));
392 Arc::new(HistogramFoldExec {
393 le_column_index,
394 field_column_index,
395 histogram_column_index,
396 ts_column_index,
397 input: exec_input,
398 tag_columns,
399 partition_exprs,
400 operation: self.operation,
401 output_schema,
402 metric: ExecutionPlanMetricsSet::new(),
403 properties,
404 })
405 }
406
407 fn convert_schema(
414 input_schema: &DFSchemaRef,
415 le_column: &str,
416 ) -> DataFusionResult<DFSchemaRef> {
417 let mut new_fields = Vec::with_capacity(input_schema.fields().len() - 1);
419 for (qualifier, field) in input_schema.iter() {
420 if field.name() != le_column {
421 new_fields.push((qualifier.cloned(), field.clone()));
422 }
423 }
424 Ok(Arc::new(DFSchema::new_with_metadata(
425 new_fields,
426 HashMap::new(),
427 )?))
428 }
429
430 pub fn serialize(&self) -> DataFusionResult<Vec<u8>> {
431 if self.histogram_column.is_some() {
432 return Err(DataFusionError::NotImplemented(
433 "mixed HistogramFold is frontend-only".to_string(),
434 ));
435 }
436 let HistogramFoldOperation::Quantile(quantile) = self.operation else {
437 return Err(DataFusionError::NotImplemented(
438 "HistogramFold fraction is frontend-only".to_string(),
439 ));
440 };
441 let le_column_idx = serialize_column_index(self.input.schema(), &self.le_column);
442 let ts_column_idx = serialize_column_index(self.input.schema(), &self.ts_column);
443 let field_column_idx = serialize_column_index(self.input.schema(), &self.field_column);
444
445 Ok(pb::HistogramFold {
446 le_column_idx,
447 ts_column_idx,
448 field_column_idx,
449 quantile: quantile.into(),
450 }
451 .encode_to_vec())
452 }
453
454 pub fn deserialize(bytes: &[u8]) -> Result<Self> {
455 let pb_histogram_fold = pb::HistogramFold::decode(bytes).context(DeserializeSnafu)?;
456 let placeholder_plan = LogicalPlan::EmptyRelation(EmptyRelation {
457 produce_one_row: false,
458 schema: Arc::new(DFSchema::empty()),
459 });
460
461 let unfix = UnfixIndices {
462 le_column_idx: pb_histogram_fold.le_column_idx,
463 ts_column_idx: pb_histogram_fold.ts_column_idx,
464 field_column_idx: pb_histogram_fold.field_column_idx,
465 };
466
467 Ok(Self {
468 le_column: String::new(),
469 ts_column: String::new(),
470 input: placeholder_plan,
471 field_column: String::new(),
472 histogram_column: None,
473 operation: HistogramFoldOperation::Quantile(pb_histogram_fold.quantile.into()),
474 output_schema: Arc::new(DFSchema::empty()),
475 unfix: Some(unfix),
476 })
477 }
478}
479
480impl PartialOrd for HistogramFold {
481 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
482 match self.le_column.partial_cmp(&other.le_column) {
484 Some(core::cmp::Ordering::Equal) => {}
485 ord => return ord,
486 }
487 match self.ts_column.partial_cmp(&other.ts_column) {
488 Some(core::cmp::Ordering::Equal) => {}
489 ord => return ord,
490 }
491 match self.input.partial_cmp(&other.input) {
492 Some(core::cmp::Ordering::Equal) => {}
493 ord => return ord,
494 }
495 match self.field_column.partial_cmp(&other.field_column) {
496 Some(core::cmp::Ordering::Equal) => {}
497 ord => return ord,
498 }
499 match self.histogram_column.partial_cmp(&other.histogram_column) {
500 Some(core::cmp::Ordering::Equal) => {}
501 ord => return ord,
502 }
503 self.operation.partial_cmp(&other.operation)
504 }
505}
506
507#[derive(Debug)]
508pub struct HistogramFoldExec {
509 le_column_index: usize,
511 input: Arc<dyn ExecutionPlan>,
512 output_schema: SchemaRef,
513 field_column_index: usize,
515 histogram_column_index: Option<usize>,
517 ts_column_index: usize,
518 tag_columns: Vec<Arc<dyn PhysicalExpr>>,
520 partition_exprs: Vec<Arc<dyn PhysicalExpr>>,
521 operation: HistogramFoldOperation,
522 metric: ExecutionPlanMetricsSet,
523 properties: Arc<PlanProperties>,
524}
525
526impl ExecutionPlan for HistogramFoldExec {
527 fn apply_expressions(
528 &self,
529 f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> datafusion_common::Result<TreeNodeRecursion>,
530 ) -> DataFusionResult<TreeNodeRecursion> {
531 datafusion::physical_plan::apply_expression_roots(
532 self.tag_columns.iter().chain(self.partition_exprs.iter()),
533 f,
534 )
535 }
536
537 fn properties(&self) -> &Arc<PlanProperties> {
538 &self.properties
539 }
540
541 fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
542 let mut cols = self
543 .tag_columns
544 .iter()
545 .map(|expr| PhysicalSortRequirement {
546 expr: expr.clone(),
547 options: None,
548 })
549 .collect::<Vec<PhysicalSortRequirement>>();
550 cols.push(PhysicalSortRequirement {
552 expr: Arc::new(PhyColumn::new(
553 self.input.schema().field(self.ts_column_index).name(),
554 self.ts_column_index,
555 )),
556 options: None,
557 });
558 if self.histogram_column_index.is_none() {
559 cols.push(PhysicalSortRequirement {
561 expr: Arc::new(PhyTryCast::new(
562 Arc::new(PhyColumn::new(
563 self.input.schema().field(self.le_column_index).name(),
564 self.le_column_index,
565 )),
566 DataType::Float64,
567 )),
568 options: Some(SortOptions {
569 descending: false, nulls_first: false, }),
572 });
573 }
574
575 let requirement = LexRequirement::new(cols).unwrap();
577
578 vec![Some(OrderingRequirements::Hard(vec![requirement]))]
579 }
580
581 fn input_distribution_requirements(&self) -> InputDistributionRequirements {
582 InputDistributionRequirements::new(vec![Distribution::KeyPartitioned(
583 self.partition_exprs.clone(),
584 )])
585 }
586
587 fn maintains_input_order(&self) -> Vec<bool> {
588 vec![self.histogram_column_index.is_none(); self.children().len()]
589 }
590
591 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
592 vec![&self.input]
593 }
594
595 fn with_new_children(
597 self: Arc<Self>,
598 children: Vec<Arc<dyn ExecutionPlan>>,
599 ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
600 assert!(!children.is_empty());
601 let new_input = children[0].clone();
602 let properties = Arc::new(PlanProperties::new(
603 EquivalenceProperties::new(self.output_schema.clone()),
604 Partitioning::UnknownPartitioning(new_input.output_partitioning().partition_count()),
605 EmissionType::Incremental,
606 Boundedness::Bounded,
607 ));
608 Ok(Arc::new(Self {
609 input: new_input,
610 metric: self.metric.clone(),
611 le_column_index: self.le_column_index,
612 ts_column_index: self.ts_column_index,
613 tag_columns: self.tag_columns.clone(),
614 partition_exprs: self.partition_exprs.clone(),
615 operation: self.operation,
616 output_schema: self.output_schema.clone(),
617 field_column_index: self.field_column_index,
618 histogram_column_index: self.histogram_column_index,
619 properties,
620 }))
621 }
622
623 fn execute(
624 &self,
625 partition: usize,
626 context: Arc<TaskContext>,
627 ) -> DataFusionResult<SendableRecordBatchStream> {
628 let baseline_metric = BaselineMetrics::new(&self.metric, partition);
629
630 let batch_size = context.session_config().batch_size();
631 let input = self.input.execute(partition, context)?;
632 let output_schema = self.output_schema.clone();
633
634 let mut normal_indices = (0..input.schema().fields().len()).collect::<HashSet<_>>();
635 normal_indices.remove(&self.field_column_index);
636 normal_indices.remove(&self.le_column_index);
637 if let Some(histogram_column_index) = self.histogram_column_index {
638 normal_indices.remove(&histogram_column_index);
639 }
640 let mode = if self.histogram_column_index.is_some() {
641 FoldMode::Safe
642 } else {
643 FoldMode::Optimistic
644 };
645 Ok(Box::pin(HistogramFoldStream {
646 le_column_index: self.le_column_index,
647 field_column_index: self.field_column_index,
648 histogram_column_index: self.histogram_column_index,
649 operation: self.operation,
650 normal_indices: normal_indices.into_iter().collect(),
651 bucket_size: None,
652 input_buffer: vec![],
653 input,
654 output_schema,
655 input_schema: self.input.schema(),
656 mode,
657 safe_group: None,
658 metric: baseline_metric,
659 batch_size,
660 input_buffered_rows: 0,
661 output_buffer: HistogramFoldStream::empty_output_buffer(&self.input.schema())?,
662 output_buffered_rows: 0,
663 }))
664 }
665
666 fn metrics(&self) -> Option<MetricsSet> {
667 Some(self.metric.clone_inner())
668 }
669
670 fn statistics_from_inputs(
671 &self,
672 _input_stats: &[Arc<Statistics>],
673 _args: &StatisticsArgs,
674 ) -> DataFusionResult<Arc<Statistics>> {
675 Ok(Arc::new(Statistics {
676 num_rows: Precision::Absent,
677 total_byte_size: Precision::Absent,
678 column_statistics: Statistics::unknown_column(&self.schema()),
679 }))
680 }
681
682 fn name(&self) -> &str {
683 "HistogramFoldExec"
684 }
685}
686
687impl DisplayAs for HistogramFoldExec {
688 fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
689 match t {
690 DisplayFormatType::Default
691 | DisplayFormatType::Verbose
692 | DisplayFormatType::TreeRender => {
693 write!(
694 f,
695 "HistogramFoldExec: le=@{}, field=@{}",
696 self.le_column_index, self.field_column_index
697 )?;
698 if let Some(histogram) = self.histogram_column_index {
699 write!(f, ", histogram=@{histogram}")?;
700 }
701 write!(f, ", {}", self.operation)
702 }
703 }
704 }
705}
706
707#[derive(Debug, Clone, Copy, PartialEq, Eq)]
708enum FoldMode {
709 Optimistic,
710 Safe,
711}
712
713pub struct HistogramFoldStream {
714 le_column_index: usize,
716 field_column_index: usize,
717 histogram_column_index: Option<usize>,
718 operation: HistogramFoldOperation,
719 normal_indices: Vec<usize>,
721 bucket_size: Option<usize>,
722 batch_size: usize,
724 output_schema: SchemaRef,
725 input_schema: SchemaRef,
726 mode: FoldMode,
727 safe_group: Option<SafeGroup>,
728
729 input_buffer: Vec<RecordBatch>,
731 input_buffered_rows: usize,
732 output_buffer: Vec<Box<dyn MutableVector>>,
733 output_buffered_rows: usize,
734
735 input: SendableRecordBatchStream,
737 metric: BaselineMetrics,
738}
739
740#[derive(Debug, Default)]
741struct SafeGroup {
742 tag_values: Vec<Value>,
743 buckets: Vec<f64>,
744 counters: Vec<f64>,
745 native_samples: Vec<(Value, Value)>,
746}
747
748impl RecordBatchStream for HistogramFoldStream {
749 fn schema(&self) -> SchemaRef {
750 self.output_schema.clone()
751 }
752}
753
754impl Stream for HistogramFoldStream {
755 type Item = DataFusionResult<RecordBatch>;
756
757 fn poll_next(
758 mut self: std::pin::Pin<&mut Self>,
759 cx: &mut std::task::Context<'_>,
760 ) -> Poll<Option<Self::Item>> {
761 let poll = loop {
762 match ready!(self.input.poll_next_unpin(cx)) {
763 Some(batch) => {
764 let batch = batch?;
765 let timer = Instant::now();
766 let Some(result) = self.fold_input(batch)? else {
767 self.metric.elapsed_compute().add_elapsed(timer);
768 continue;
769 };
770 self.metric.elapsed_compute().add_elapsed(timer);
771 break Poll::Ready(Some(result));
772 }
773 None => {
774 self.flush_remaining()?;
775 break Poll::Ready(self.take_output_buf()?.map(Ok));
776 }
777 }
778 };
779 self.metric.record_poll(poll)
780 }
781}
782
783impl HistogramFoldStream {
784 pub fn fold_input(
786 &mut self,
787 input: RecordBatch,
788 ) -> DataFusionResult<Option<DataFusionResult<RecordBatch>>> {
789 match self.mode {
790 FoldMode::Safe => {
791 self.push_input_buf(input);
792 self.process_safe_mode_buffer()?;
793 }
794 FoldMode::Optimistic => {
795 self.push_input_buf(input);
796 let Some(bucket_num) = self.calculate_bucket_num_from_buffer()? else {
797 return Ok(None);
798 };
799 self.bucket_size = Some(bucket_num);
800
801 if self.input_buffered_rows < bucket_num {
802 return Ok(None);
804 }
805
806 self.fold_buf(bucket_num)?;
807 }
808 }
809
810 self.maybe_take_output()
811 }
812
813 pub fn empty_output_buffer(
815 schema: &SchemaRef,
816 ) -> DataFusionResult<Vec<Box<dyn MutableVector>>> {
817 let mut builders = Vec::with_capacity(schema.fields().len());
818 for field in schema.fields() {
819 let concrete_datatype = ConcreteDataType::try_from(field.data_type()).unwrap();
820 let mutable_vector = concrete_datatype.create_mutable_vector(0);
821 builders.push(mutable_vector);
822 }
823
824 Ok(builders)
825 }
826
827 fn calculate_bucket_num_from_buffer(&mut self) -> DataFusionResult<Option<usize>> {
830 if let Some(size) = self.bucket_size {
831 return Ok(Some(size));
832 }
833
834 if self.input_buffer.is_empty() {
835 return Ok(None);
836 }
837
838 let batch_refs: Vec<&RecordBatch> = self.input_buffer.iter().collect();
839 let batch = concat_batches(&self.input_schema, batch_refs)?;
840 self.find_first_complete_bucket(&batch)
841 }
842
843 fn find_first_complete_bucket(&self, batch: &RecordBatch) -> DataFusionResult<Option<usize>> {
846 if batch.num_rows() == 0 {
847 return Ok(None);
848 }
849
850 let vectors = Helper::try_into_vectors(batch.columns())
851 .map_err(|e| DataFusionError::Execution(e.to_string()))?;
852 let mut tag_values_buf = Vec::with_capacity(self.normal_indices.len());
853 self.collect_tag_values(&vectors, 0, &mut tag_values_buf);
854
855 for row in 1..batch.num_rows() {
856 if !self.is_same_group(&vectors, row, &tag_values_buf) {
857 return Ok(Some(row));
860 }
861 }
862
863 Ok(None)
864 }
865
866 fn fold_buf(&mut self, bucket_num: usize) -> DataFusionResult<()> {
868 let batch = concat_batches(&self.input_schema, self.input_buffer.drain(..).as_ref())?;
869 let mut remaining_rows = self.input_buffered_rows;
870 let mut cursor = 0;
871
872 let vectors = Helper::try_into_vectors(batch.columns())
874 .map_err(|e| DataFusionError::Execution(e.to_string()))?;
875 let le_array = batch.column(self.le_column_index);
876 let field_array = batch.column(self.field_column_index);
877 let field_array = field_array.as_primitive::<Float64Type>();
878 let mut tag_values_buf = Vec::with_capacity(self.normal_indices.len());
879
880 while remaining_rows > bucket_num && self.mode == FoldMode::Optimistic {
884 self.collect_tag_values(&vectors, cursor, &mut tag_values_buf);
885 if !self.validate_optimistic_group(
886 &vectors,
887 le_array,
888 cursor,
889 bucket_num,
890 &tag_values_buf,
891 ) {
892 let remaining_input_batch = batch.slice(cursor, remaining_rows);
893 self.switch_to_safe_mode(remaining_input_batch)?;
894 return Ok(());
895 }
896
897 for (idx, value) in self.normal_indices.iter().zip(tag_values_buf.iter()) {
899 self.output_buffer[*idx].push_value_ref(value);
900 }
901 let mut bucket = Vec::with_capacity(bucket_num);
903 let mut counters = Vec::with_capacity(bucket_num);
904 for bias in 0..bucket_num {
905 let position = cursor + bias;
906 let le = string_array_value_at_index(le_array, position)
907 .and_then(|value| value.parse::<f64>().ok())
908 .unwrap_or(f64::NAN);
909 bucket.push(le);
910
911 let counter = if field_array.is_valid(position) {
912 field_array.value(position)
913 } else {
914 f64::NAN
915 };
916 counters.push(counter);
917 }
918 let result = Self::evaluate_row(self.operation, &bucket, &counters).unwrap_or(f64::NAN);
920 self.output_buffer[self.field_column_index].push_value_ref(&ValueRef::from(result));
921 cursor += bucket_num;
922 remaining_rows -= bucket_num;
923 self.output_buffered_rows += 1;
924 }
925
926 let remaining_input_batch = batch.slice(cursor, remaining_rows);
927 self.input_buffered_rows = remaining_input_batch.num_rows();
928 if self.input_buffered_rows > 0 {
929 self.input_buffer.push(remaining_input_batch);
930 }
931
932 Ok(())
933 }
934
935 fn push_input_buf(&mut self, batch: RecordBatch) {
936 self.input_buffered_rows += batch.num_rows();
937 self.input_buffer.push(batch);
938 }
939
940 fn maybe_take_output(&mut self) -> DataFusionResult<Option<DataFusionResult<RecordBatch>>> {
941 if self.output_buffered_rows >= self.batch_size {
942 return Ok(self.take_output_buf()?.map(Ok));
943 }
944 Ok(None)
945 }
946
947 fn switch_to_safe_mode(&mut self, remaining_batch: RecordBatch) -> DataFusionResult<()> {
948 self.mode = FoldMode::Safe;
949 self.bucket_size = None;
950 self.input_buffer.clear();
951 self.input_buffered_rows = remaining_batch.num_rows();
952
953 if self.input_buffered_rows > 0 {
954 self.input_buffer.push(remaining_batch);
955 self.process_safe_mode_buffer()?;
956 }
957
958 Ok(())
959 }
960
961 fn collect_tag_values<'a>(
962 &self,
963 vectors: &'a [VectorRef],
964 row: usize,
965 tag_values: &mut Vec<ValueRef<'a>>,
966 ) {
967 tag_values.clear();
968 for idx in self.normal_indices.iter() {
969 tag_values.push(vectors[*idx].get_ref(row));
970 }
971 }
972
973 fn validate_optimistic_group(
974 &self,
975 vectors: &[VectorRef],
976 le_array: &ArrayRef,
977 cursor: usize,
978 bucket_num: usize,
979 tag_values: &[ValueRef<'_>],
980 ) -> bool {
981 let inf_index = cursor + bucket_num - 1;
982 if self.is_same_group(vectors, inf_index + 1, tag_values) {
983 return false;
984 }
985 if !Self::is_positive_infinity(le_array, inf_index) {
986 return false;
987 }
988 if (cursor..=inf_index).any(|row| {
989 string_array_value_at_index(le_array, row)
990 .and_then(|value| value.parse::<f64>().ok())
991 .is_none()
992 }) {
993 return false;
994 }
995
996 for offset in 1..bucket_num {
997 let row = cursor + offset;
998 for (idx, expected) in self.normal_indices.iter().zip(tag_values.iter()) {
999 if vectors[*idx].get_ref(row) != *expected {
1000 return false;
1001 }
1002 }
1003 }
1004 true
1005 }
1006
1007 fn is_same_group(
1009 &self,
1010 vectors: &[VectorRef],
1011 row: usize,
1012 tag_values: &[ValueRef<'_>],
1013 ) -> bool {
1014 self.normal_indices
1015 .iter()
1016 .zip(tag_values.iter())
1017 .all(|(idx, expected)| vectors[*idx].get_ref(row) == *expected)
1018 }
1019
1020 fn push_output_row(&mut self, tag_values: &[ValueRef<'_>], result: f64) {
1021 debug_assert_eq!(self.normal_indices.len(), tag_values.len());
1022 for (idx, value) in self.normal_indices.iter().zip(tag_values.iter()) {
1023 self.output_buffer[*idx].push_value_ref(value);
1024 }
1025 self.output_buffer[self.field_column_index].push_value_ref(&ValueRef::from(result));
1026 self.output_buffered_rows += 1;
1027 }
1028
1029 fn push_mixed_output_row(
1030 &mut self,
1031 tag_values: &[Value],
1032 le: &Value,
1033 result: Option<f64>,
1034 histogram: &Value,
1035 ) {
1036 let histogram_column_index = self.histogram_column_index.unwrap();
1037 for (idx, value) in self.normal_indices.iter().zip(tag_values) {
1038 self.output_buffer[*idx].push_value_ref(&value.as_value_ref());
1039 }
1040 self.output_buffer[self.le_column_index].push_value_ref(&le.as_value_ref());
1041 self.output_buffer[self.field_column_index]
1042 .push_value_ref(&result.map_or(ValueRef::Null, ValueRef::from));
1043 self.output_buffer[histogram_column_index].push_value_ref(&histogram.as_value_ref());
1044 self.output_buffered_rows += 1;
1045 }
1046
1047 fn finalize_safe_group(&mut self) -> DataFusionResult<()> {
1048 let Some(group) = self.safe_group.take() else {
1049 return Ok(());
1050 };
1051 if group.tag_values.is_empty() {
1052 return Ok(());
1053 }
1054
1055 if self.histogram_column_index.is_some() {
1056 let classic_result = if group.buckets.is_empty() {
1057 None
1058 } else {
1059 let mut buckets = group
1060 .buckets
1061 .into_iter()
1062 .zip(group.counters)
1063 .collect::<Vec<_>>();
1064 buckets.sort_by(|lhs, rhs| lhs.0.total_cmp(&rhs.0));
1065 let (bounds, counters): (Vec<_>, Vec<_>) = buckets.into_iter().unzip();
1066 let has_inf = bounds
1067 .last()
1068 .is_some_and(|value| value.is_infinite() && value.is_sign_positive());
1069 Some(if has_inf {
1070 Self::evaluate_row(self.operation, &bounds, &counters).unwrap_or(f64::NAN)
1071 } else {
1072 f64::NAN
1073 })
1074 };
1075 let has_null_native = group.native_samples.iter().any(|(le, _)| le.is_null());
1076 for (le, histogram) in group.native_samples.iter().filter(|(le, _)| !le.is_null()) {
1077 self.push_mixed_output_row(&group.tag_values, le, None, histogram);
1078 }
1079 for (le, histogram) in group.native_samples.iter().filter(|(le, _)| le.is_null()) {
1080 self.push_mixed_output_row(&group.tag_values, le, classic_result, histogram);
1081 }
1082 if !has_null_native && let Some(result) = classic_result {
1083 self.push_mixed_output_row(
1084 &group.tag_values,
1085 &Value::Null,
1086 Some(result),
1087 &Value::Null,
1088 );
1089 }
1090 return Ok(());
1091 }
1092 if group.buckets.is_empty() {
1093 return Ok(());
1094 }
1095
1096 let has_inf = group
1097 .buckets
1098 .last()
1099 .is_some_and(|value| value.is_infinite() && value.is_sign_positive());
1100 let result = if has_inf {
1101 Self::evaluate_row(self.operation, &group.buckets, &group.counters).unwrap_or(f64::NAN)
1102 } else {
1103 f64::NAN
1104 };
1105 let tag_value_refs = group
1106 .tag_values
1107 .iter()
1108 .map(Value::as_value_ref)
1109 .collect::<Vec<_>>();
1110 self.push_output_row(&tag_value_refs, result);
1111 Ok(())
1112 }
1113
1114 fn process_safe_mode_buffer(&mut self) -> DataFusionResult<()> {
1115 if self.input_buffer.is_empty() {
1116 self.input_buffered_rows = 0;
1117 return Ok(());
1118 }
1119
1120 let batch = concat_batches(&self.input_schema, self.input_buffer.drain(..).as_ref())?;
1121 self.input_buffered_rows = 0;
1122 let vectors = Helper::try_into_vectors(batch.columns())
1123 .map_err(|e| DataFusionError::Execution(e.to_string()))?;
1124 let le_array = batch.column(self.le_column_index);
1125 let field_array = batch
1126 .column(self.field_column_index)
1127 .as_primitive::<Float64Type>();
1128 let mut tag_values_buf = Vec::with_capacity(self.normal_indices.len());
1129
1130 for row in 0..batch.num_rows() {
1131 self.collect_tag_values(&vectors, row, &mut tag_values_buf);
1132 let should_start_new_group = self
1133 .safe_group
1134 .as_ref()
1135 .is_none_or(|group| !Self::tag_values_equal(&group.tag_values, &tag_values_buf));
1136 if should_start_new_group {
1137 self.finalize_safe_group()?;
1138 self.safe_group = Some(SafeGroup {
1139 tag_values: tag_values_buf.iter().cloned().map(Value::from).collect(),
1140 buckets: Vec::new(),
1141 counters: Vec::new(),
1142 native_samples: Vec::new(),
1143 });
1144 }
1145
1146 let Some(group) = self.safe_group.as_mut() else {
1147 continue;
1148 };
1149
1150 let mixed = self.histogram_column_index.is_some();
1151 let bucket = string_array_value_at_index(le_array, row)
1152 .and_then(|value| value.parse::<f64>().ok());
1153 if let Some(bucket) = bucket
1154 && (!mixed || field_array.is_valid(row))
1155 {
1156 let counter = if field_array.is_valid(row) {
1157 field_array.value(row)
1158 } else {
1159 f64::NAN
1160 };
1161 group.buckets.push(bucket);
1162 group.counters.push(counter);
1163 }
1164 if let Some(histogram_column_index) = self.histogram_column_index {
1165 let histogram = vectors[histogram_column_index].get(row);
1166 if !histogram.is_null() {
1167 group
1168 .native_samples
1169 .push((vectors[self.le_column_index].get(row), histogram));
1170 }
1171 }
1172 }
1173
1174 Ok(())
1175 }
1176
1177 fn tag_values_equal(group_values: &[Value], current: &[ValueRef<'_>]) -> bool {
1178 group_values.len() == current.len()
1179 && group_values
1180 .iter()
1181 .zip(current.iter())
1182 .all(|(group, now)| group.as_value_ref() == *now)
1183 }
1184
1185 fn take_output_buf(&mut self) -> DataFusionResult<Option<RecordBatch>> {
1187 if self.output_buffered_rows == 0 {
1188 if self.input_buffered_rows != 0 {
1189 warn!(
1190 "input buffer is not empty, {} rows remaining",
1191 self.input_buffered_rows
1192 );
1193 }
1194 return Ok(None);
1195 }
1196
1197 let mut output_buf = Self::empty_output_buffer(&self.input_schema)?;
1198 std::mem::swap(&mut self.output_buffer, &mut output_buf);
1199 let mut columns = Vec::with_capacity(output_buf.len());
1200 for builder in output_buf.iter_mut() {
1201 columns.push(builder.to_vector().to_arrow_array());
1202 }
1203 if self.histogram_column_index.is_none() {
1204 columns.remove(self.le_column_index);
1205 }
1206
1207 self.output_buffered_rows = 0;
1208 RecordBatch::try_new(self.output_schema.clone(), columns)
1209 .map(Some)
1210 .map_err(|e| DataFusionError::ArrowError(Box::new(e), None))
1211 }
1212
1213 fn flush_remaining(&mut self) -> DataFusionResult<()> {
1214 if self.mode == FoldMode::Optimistic && self.input_buffered_rows > 0 {
1215 let buffered_batches: Vec<_> = std::mem::take(&mut self.input_buffer);
1216 if !buffered_batches.is_empty() {
1217 let batch = concat_batches(&self.input_schema, buffered_batches.as_slice())?;
1218 self.switch_to_safe_mode(batch)?;
1219 } else {
1220 self.input_buffered_rows = 0;
1221 }
1222 }
1223
1224 if self.mode == FoldMode::Safe {
1225 self.process_safe_mode_buffer()?;
1226 self.finalize_safe_group()?;
1227 }
1228
1229 Ok(())
1230 }
1231
1232 fn is_positive_infinity(le_array: &ArrayRef, index: usize) -> bool {
1233 matches!(
1234 string_array_value_at_index(le_array, index).and_then(|value| value.parse::<f64>().ok()),
1235 Some(value) if value.is_infinite() && value.is_sign_positive()
1236 )
1237 }
1238
1239 fn evaluate_row(
1241 operation: HistogramFoldOperation,
1242 bucket: &[f64],
1243 counter: &[f64],
1244 ) -> DataFusionResult<f64> {
1245 if bucket.is_empty()
1247 || matches!(operation, HistogramFoldOperation::Quantile(_)) && bucket.len() == 1
1248 {
1249 return Ok(f64::NAN);
1250 }
1251 if bucket.last() != Some(&f64::INFINITY) {
1252 return Err(DataFusionError::Execution(
1253 "last bucket should be +Inf".to_string(),
1254 ));
1255 }
1256 if bucket.len() != counter.len() {
1257 return Err(DataFusionError::Execution(
1258 "bucket and counter should have the same length".to_string(),
1259 ));
1260 }
1261 if let HistogramFoldOperation::Quantile(quantile) = operation {
1262 let quantile = f64::from(quantile);
1263 if quantile < 0.0 {
1264 return Ok(f64::NEG_INFINITY);
1265 } else if quantile > 1.0 {
1266 return Ok(f64::INFINITY);
1267 } else if quantile.is_nan() {
1268 return Ok(f64::NAN);
1269 }
1270 }
1271
1272 let has_duplicates = !bucket.windows(2).all(|w| w[0] < w[1]);
1275 if has_duplicates && !bucket.windows(2).all(|w| w[0] <= w[1]) {
1276 return Ok(f64::NAN);
1277 }
1278 let coalesced = has_duplicates.then(|| {
1280 let mut bounds = Vec::with_capacity(bucket.len());
1281 let mut counts = Vec::with_capacity(counter.len());
1282 for (&bound, &count) in bucket.iter().zip(counter) {
1283 if bounds.last() == Some(&bound) {
1284 *counts.last_mut().unwrap() += count;
1285 } else {
1286 bounds.push(bound);
1287 counts.push(count);
1288 }
1289 }
1290 (bounds, counts)
1291 });
1292 let (bucket, counter) = match &coalesced {
1293 Some((bounds, counts)) => (bounds.as_slice(), counts.as_slice()),
1294 None => (bucket, counter),
1295 };
1296 if matches!(operation, HistogramFoldOperation::Quantile(_)) && bucket.len() < 2 {
1297 return Ok(f64::NAN);
1298 }
1299 let counter = match operation {
1300 HistogramFoldOperation::Quantile(_) => {
1301 let needs_fix = counter.iter().any(|v| !v.is_finite())
1302 || !counter.windows(2).all(|w| w[0] <= w[1]);
1303 if !needs_fix {
1304 Cow::Borrowed(counter)
1305 } else {
1306 let mut fixed = Vec::with_capacity(counter.len());
1307 let mut prev = 0.0;
1308 for (idx, &v) in counter.iter().enumerate() {
1309 let mut val = if v.is_finite() { v } else { prev };
1310 if idx > 0 && val < prev {
1311 val = prev;
1312 }
1313 fixed.push(val);
1314 prev = val;
1315 }
1316 Cow::Owned(fixed)
1317 }
1318 }
1319 HistogramFoldOperation::Fraction { .. } => Cow::Borrowed(counter),
1320 };
1321
1322 Ok(match operation {
1323 HistogramFoldOperation::Quantile(quantile) => {
1324 Self::evaluate_quantile(quantile.into(), bucket, &counter)
1325 }
1326 HistogramFoldOperation::Fraction { lower, upper } => {
1327 Self::evaluate_fraction(lower.into(), upper.into(), bucket, &counter)
1328 }
1329 })
1330 }
1331
1332 fn evaluate_quantile(quantile: f64, bucket: &[f64], counter: &[f64]) -> f64 {
1333 let total = *counter.last().unwrap();
1334 let expected_pos = total * quantile;
1335 let mut fit_bucket_pos = 0;
1336 while fit_bucket_pos < bucket.len() && counter[fit_bucket_pos] < expected_pos {
1337 fit_bucket_pos += 1;
1338 }
1339 if fit_bucket_pos >= bucket.len() - 1 {
1340 bucket[bucket.len() - 2]
1341 } else {
1342 let upper_bound = bucket[fit_bucket_pos];
1343 let upper_count = counter[fit_bucket_pos];
1344 let mut lower_bound = bucket[0].min(0.0);
1345 let mut lower_count = 0.0;
1346 if fit_bucket_pos > 0 {
1347 lower_bound = bucket[fit_bucket_pos - 1];
1348 lower_count = counter[fit_bucket_pos - 1];
1349 }
1350 if (upper_count - lower_count).abs() < 1e-10 {
1351 return f64::NAN;
1352 }
1353 lower_bound
1354 + (upper_bound - lower_bound) / (upper_count - lower_count)
1355 * (expected_pos - lower_count)
1356 }
1357 }
1358
1359 fn evaluate_fraction(lower: f64, upper: f64, bucket: &[f64], counter: &[f64]) -> f64 {
1360 let total = *counter.last().unwrap();
1361 if total == 0.0 || lower.is_nan() || upper.is_nan() {
1362 return f64::NAN;
1363 }
1364 if lower >= upper {
1365 return 0.0;
1366 }
1367
1368 let mut rank = 0.0;
1369 let mut lower_rank = 0.0;
1370 let mut upper_rank = 0.0;
1371 let mut lower_set = false;
1372 let mut upper_set = false;
1373 let mut lower_bound = if bucket[0] > 0.0 {
1374 0.0
1375 } else {
1376 f64::NEG_INFINITY
1377 };
1378
1379 for (idx, (&upper_bound, &upper_count)) in bucket.iter().zip(counter).enumerate() {
1380 if idx > 0 {
1381 lower_bound = bucket[idx - 1];
1382 }
1383 let interpolate = |value: f64| {
1384 if lower_bound == f64::NEG_INFINITY {
1385 upper_count
1386 } else {
1387 rank + (upper_count - rank) * (value - lower_bound)
1388 / (upper_bound - lower_bound)
1389 }
1390 };
1391
1392 if !lower_set && lower_bound >= lower {
1393 lower_rank = rank;
1394 lower_set = true;
1395 }
1396 if !upper_set && lower_bound >= upper {
1397 upper_rank = rank;
1398 upper_set = true;
1399 }
1400 if lower_set && upper_set {
1401 break;
1402 }
1403 if !lower_set && lower_bound < lower && upper_bound > lower {
1404 lower_rank = interpolate(lower);
1405 lower_set = true;
1406 }
1407 if !upper_set && lower_bound < upper && upper_bound > upper {
1408 upper_rank = interpolate(upper);
1409 upper_set = true;
1410 }
1411 if lower_set && upper_set {
1412 break;
1413 }
1414 rank = upper_count;
1415 }
1416
1417 if !lower_set || lower_rank > total {
1418 lower_rank = total;
1419 }
1420 if !upper_set || upper_rank > total {
1421 upper_rank = total;
1422 }
1423 (upper_rank - lower_rank) / total
1424 }
1425}
1426
1427#[cfg(test)]
1428mod test {
1429 use std::sync::Arc;
1430
1431 use datafusion::arrow::array::{
1432 DictionaryArray, Float64Array, StringDictionaryBuilder, TimestampMillisecondArray,
1433 };
1434 use datafusion::arrow::datatypes::{Field, Schema, SchemaRef, TimeUnit, UInt32Type};
1435 use datafusion::common::ToDFSchema;
1436 use datafusion::datasource::memory::MemorySourceConfig;
1437 use datafusion::datasource::source::DataSourceExec;
1438 use datafusion::logical_expr::EmptyRelation;
1439 use datafusion::physical_plan::{ChildrenPropertiesMode, ReplaceChildrenOptions};
1440 use datafusion::prelude::SessionContext;
1441 use datatypes::arrow_array::StringArray;
1442 use futures::FutureExt;
1443
1444 use super::*;
1445
1446 fn project_batch(batch: &RecordBatch, indices: &[usize]) -> RecordBatch {
1447 let fields = indices
1448 .iter()
1449 .map(|&idx| batch.schema().field(idx).clone())
1450 .collect::<Vec<_>>();
1451 let columns = indices
1452 .iter()
1453 .map(|&idx| batch.column(idx).clone())
1454 .collect::<Vec<_>>();
1455 let schema = Arc::new(Schema::new(fields));
1456 RecordBatch::try_new(schema, columns).unwrap()
1457 }
1458
1459 fn prepare_test_data_batches() -> (Vec<RecordBatch>, SchemaRef) {
1462 let schema = Arc::new(Schema::new(vec![
1463 Field::new("host", DataType::Utf8, true),
1464 Field::new("le", DataType::Utf8, true),
1465 Field::new("val", DataType::Float64, true),
1466 ]));
1467
1468 let host_column_1 = Arc::new(StringArray::from(vec![
1470 "host_1", "host_1", "host_1", "host_1", "host_1", "host_2", "host_2", "host_2",
1471 "host_2", "host_2", "host_3", "host_3",
1472 ])) as _;
1473 let le_column_1 = Arc::new(StringArray::from(vec![
1474 "0.001", "0.1", "10", "1000", "+Inf", "0.001", "0.1", "10", "1000", "+inf", "0.001",
1475 "0.1",
1476 ])) as _;
1477 let val_column_1 = Arc::new(Float64Array::from(vec![
1478 0_0.0, 1.0, 1.0, 5.0, 5.0, 0_0.0, 20.0, 60.0, 70.0, 100.0, 0_1.0, 1.0,
1479 ])) as _;
1480
1481 let host_column_2 = Arc::new(StringArray::from(vec!["host_3", "host_3"])) as _;
1483 let le_column_2 = Arc::new(StringArray::from(vec!["10", "1000"])) as _;
1484 let val_column_2 = Arc::new(Float64Array::from(vec![1.0, 1.0])) as _;
1485
1486 let host_column_3 = Arc::new(StringArray::from(vec![
1488 "host_3", "host_4", "host_4", "host_4", "host_4", "host_4", "host_5", "host_5",
1489 "host_5", "host_5", "host_5",
1490 ])) as _;
1491 let le_column_3 = Arc::new(StringArray::from(vec![
1492 "+INF", "0.001", "0.1", "10", "1000", "+iNf", "0.001", "0.1", "10", "1000", "+Inf",
1493 ])) as _;
1494 let val_column_3 = Arc::new(Float64Array::from(vec![
1495 1.0, 0_0.0, 0.0, 0.0, 0.0, 0.0, 0_0.0, 1.0, 2.0, 3.0, 4.0,
1496 ])) as _;
1497
1498 let data_1 = RecordBatch::try_new(
1499 schema.clone(),
1500 vec![host_column_1, le_column_1, val_column_1],
1501 )
1502 .unwrap();
1503 let data_2 = RecordBatch::try_new(
1504 schema.clone(),
1505 vec![host_column_2, le_column_2, val_column_2],
1506 )
1507 .unwrap();
1508 let data_3 = RecordBatch::try_new(
1509 schema.clone(),
1510 vec![host_column_3, le_column_3, val_column_3],
1511 )
1512 .unwrap();
1513
1514 (vec![data_1, data_2, data_3], schema)
1515 }
1516
1517 fn prepare_test_data() -> DataSourceExec {
1518 let (batches, schema) = prepare_test_data_batches();
1519 DataSourceExec::new(Arc::new(
1520 MemorySourceConfig::try_new(&[batches], schema, None).unwrap(),
1521 ))
1522 }
1523
1524 fn prepare_test_data_with_two_partitions() -> DataSourceExec {
1528 let (mut batches, schema) = prepare_test_data_batches();
1529 let first_partition = vec![batches.remove(0)];
1530 DataSourceExec::new(Arc::new(
1531 MemorySourceConfig::try_new(&[first_partition, batches], schema, None).unwrap(),
1532 ))
1533 }
1534
1535 fn build_fold_exec_from_batches(
1536 batches: Vec<RecordBatch>,
1537 schema: SchemaRef,
1538 quantile: f64,
1539 ts_column_index: usize,
1540 ) -> Arc<HistogramFoldExec> {
1541 build_fold_exec_from_batches_with_operation(
1542 batches,
1543 schema,
1544 HistogramFoldOperation::Quantile(quantile.into()),
1545 ts_column_index,
1546 )
1547 }
1548
1549 fn build_fold_exec_from_batches_with_operation(
1550 batches: Vec<RecordBatch>,
1551 schema: SchemaRef,
1552 operation: HistogramFoldOperation,
1553 ts_column_index: usize,
1554 ) -> Arc<HistogramFoldExec> {
1555 let input: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
1556 MemorySourceConfig::try_new(&[batches], schema.clone(), None).unwrap(),
1557 )));
1558 let output_schema: SchemaRef = Arc::new(
1559 HistogramFold::convert_schema(&Arc::new(input.schema().to_dfschema().unwrap()), "le")
1560 .unwrap()
1561 .as_arrow()
1562 .clone(),
1563 );
1564
1565 let (tag_columns, partition_exprs, properties) =
1566 build_test_plan_properties(&input, output_schema.clone(), ts_column_index);
1567
1568 Arc::new(HistogramFoldExec {
1569 le_column_index: 1,
1570 field_column_index: 2,
1571 histogram_column_index: None,
1572 operation,
1573 ts_column_index,
1574 input,
1575 output_schema,
1576 tag_columns,
1577 partition_exprs,
1578 metric: ExecutionPlanMetricsSet::new(),
1579 properties,
1580 })
1581 }
1582
1583 type PlanPropsResult = (
1584 Vec<Arc<dyn PhysicalExpr>>,
1585 Vec<Arc<dyn PhysicalExpr>>,
1586 Arc<PlanProperties>,
1587 );
1588
1589 fn build_test_plan_properties(
1590 input: &Arc<dyn ExecutionPlan>,
1591 output_schema: SchemaRef,
1592 ts_column_index: usize,
1593 ) -> PlanPropsResult {
1594 let tag_columns = input
1595 .schema()
1596 .fields()
1597 .iter()
1598 .enumerate()
1599 .filter_map(|(idx, field)| {
1600 if idx == 1 || idx == 2 || idx == ts_column_index {
1601 None
1602 } else {
1603 Some(Arc::new(PhyColumn::new(field.name(), idx)) as _)
1604 }
1605 })
1606 .collect::<Vec<_>>();
1607
1608 let partition_exprs = if tag_columns.is_empty() {
1609 vec![Arc::new(PhyColumn::new(
1610 input.schema().field(ts_column_index).name(),
1611 ts_column_index,
1612 )) as _]
1613 } else {
1614 tag_columns.clone()
1615 };
1616
1617 let properties = PlanProperties::new(
1618 EquivalenceProperties::new(output_schema.clone()),
1619 Partitioning::UnknownPartitioning(
1620 input.properties().output_partitioning().partition_count(),
1621 ),
1622 EmissionType::Incremental,
1623 Boundedness::Bounded,
1624 );
1625
1626 (tag_columns, partition_exprs, Arc::new(properties))
1627 }
1628
1629 #[tokio::test]
1630 async fn fold_overall() {
1631 let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(prepare_test_data());
1632 let output_schema: SchemaRef = Arc::new(
1633 HistogramFold::convert_schema(
1634 &Arc::new(memory_exec.schema().to_dfschema().unwrap()),
1635 "le",
1636 )
1637 .unwrap()
1638 .as_arrow()
1639 .clone(),
1640 );
1641 let (tag_columns, partition_exprs, properties) =
1642 build_test_plan_properties(&memory_exec, output_schema.clone(), 0);
1643 let fold_exec = Arc::new(HistogramFoldExec {
1644 le_column_index: 1,
1645 field_column_index: 2,
1646 histogram_column_index: None,
1647 operation: HistogramFoldOperation::Quantile(0.4.into()),
1648 ts_column_index: 0,
1649 input: memory_exec,
1650 output_schema,
1651 tag_columns,
1652 partition_exprs,
1653 metric: ExecutionPlanMetricsSet::new(),
1654 properties,
1655 });
1656
1657 let session_context = SessionContext::default();
1658 let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
1659 .await
1660 .unwrap();
1661 let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
1662 .unwrap()
1663 .to_string();
1664
1665 let expected = String::from(
1666 "+--------+-------------------+
1667| host | val |
1668+--------+-------------------+
1669| host_1 | 257.5 |
1670| host_2 | 5.05 |
1671| host_3 | 0.0004 |
1672| host_4 | NaN |
1673| host_5 | 6.040000000000001 |
1674+--------+-------------------+",
1675 );
1676 assert_eq!(result_literal, expected);
1677 }
1678
1679 #[tokio::test]
1680 async fn fold_dictionary_encoded_labels() {
1681 let dictionary_type =
1682 DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Utf8));
1683 let schema = Arc::new(Schema::new(vec![
1684 Field::new("host", dictionary_type.clone(), true),
1685 Field::new("le", dictionary_type, true),
1686 Field::new("val", DataType::Float64, true),
1687 ]));
1688
1689 let mut host = StringDictionaryBuilder::<UInt32Type>::new();
1690 let mut le = StringDictionaryBuilder::<UInt32Type>::new();
1691 for value in ["host_1", "host_1", "host_1"] {
1692 host.append_value(value);
1693 }
1694 for value in ["0.1", "1", "+Inf"] {
1695 le.append_value(value);
1696 }
1697 let batch = RecordBatch::try_new(
1698 schema.clone(),
1699 vec![
1700 Arc::new(host.finish()),
1701 Arc::new(le.finish()),
1702 Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0])),
1703 ],
1704 )
1705 .unwrap();
1706
1707 let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
1708 let result =
1709 datafusion::physical_plan::collect(fold_exec, SessionContext::default().task_ctx())
1710 .await
1711 .unwrap();
1712
1713 assert_eq!(result.len(), 1);
1714 assert_eq!(result[0].num_rows(), 1);
1715 let host = result[0]
1716 .column(0)
1717 .as_any()
1718 .downcast_ref::<DictionaryArray<UInt32Type>>()
1719 .unwrap();
1720 assert_eq!(host.values().len(), 1);
1721 assert_eq!(
1722 string_array_value_at_index(result[0].column(0), 0),
1723 Some("host_1")
1724 );
1725 let value = result[0].column(1).as_primitive::<Float64Type>().value(0);
1726 assert!((value - 0.55).abs() < 1e-12);
1727 }
1728
1729 #[tokio::test]
1730 async fn pruning_should_keep_le_column_for_exec() {
1731 let schema = Arc::new(Schema::new(vec![
1732 Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
1733 Field::new("le", DataType::Utf8, true),
1734 Field::new("val", DataType::Float64, true),
1735 ]));
1736 let df_schema = schema.clone().to_dfschema_ref().unwrap();
1737 let input = LogicalPlan::EmptyRelation(EmptyRelation {
1738 produce_one_row: false,
1739 schema: df_schema,
1740 });
1741 let plan = HistogramFold::new(
1742 "le".to_string(),
1743 "val".to_string(),
1744 "ts".to_string(),
1745 0.5,
1746 input,
1747 )
1748 .unwrap();
1749
1750 let output_columns = [0usize, 1usize];
1751 let required = plan.necessary_children_exprs(&output_columns).unwrap();
1752 let required = &required[0];
1753 assert_eq!(required.as_slice(), &[0, 1, 2]);
1754
1755 let input_batch = RecordBatch::try_new(
1756 schema,
1757 vec![
1758 Arc::new(TimestampMillisecondArray::from(vec![0, 0])),
1759 Arc::new(StringArray::from(vec!["0.1", "+Inf"])),
1760 Arc::new(Float64Array::from(vec![1.0, 2.0])),
1761 ],
1762 )
1763 .unwrap();
1764 let projected = project_batch(&input_batch, required);
1765 let projected_schema = projected.schema();
1766 let memory_exec = Arc::new(DataSourceExec::new(Arc::new(
1767 MemorySourceConfig::try_new(&[vec![projected]], projected_schema, None).unwrap(),
1768 )));
1769
1770 let fold_exec = plan.to_execution_plan(memory_exec);
1771 let session_context = SessionContext::default();
1772 let output_batches =
1773 datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
1774 .await
1775 .unwrap();
1776 assert_eq!(output_batches.len(), 1);
1777
1778 let output_batch = &output_batches[0];
1779 assert_eq!(output_batch.num_rows(), 1);
1780
1781 let ts = output_batch
1782 .column(0)
1783 .as_any()
1784 .downcast_ref::<TimestampMillisecondArray>()
1785 .unwrap();
1786 assert_eq!(ts.values(), &[0i64]);
1787
1788 let values = output_batch
1789 .column(1)
1790 .as_any()
1791 .downcast_ref::<Float64Array>()
1792 .unwrap();
1793 assert!((values.value(0) - 0.1).abs() < 1e-12);
1794
1795 let le_index = 1usize;
1797 let broken_required = output_columns
1798 .iter()
1799 .map(|&output_column| {
1800 if output_column < le_index {
1801 output_column
1802 } else {
1803 output_column + 1
1804 }
1805 })
1806 .collect::<Vec<_>>();
1807
1808 let broken = project_batch(&input_batch, &broken_required);
1809 let broken_schema = broken.schema();
1810 let broken_exec = Arc::new(DataSourceExec::new(Arc::new(
1811 MemorySourceConfig::try_new(&[vec![broken]], broken_schema, None).unwrap(),
1812 )));
1813 let broken_fold_exec = plan.to_execution_plan(broken_exec);
1814 let session_context = SessionContext::default();
1815 let broken_result = std::panic::AssertUnwindSafe(async {
1816 datafusion::physical_plan::collect(broken_fold_exec, session_context.task_ctx()).await
1817 })
1818 .catch_unwind()
1819 .await;
1820 assert!(broken_result.is_err());
1821 }
1822
1823 #[test]
1824 fn fold_does_not_claim_hash_partitioning() {
1825 let input: Arc<dyn ExecutionPlan> = Arc::new(prepare_test_data_with_two_partitions());
1826 let logical_input = LogicalPlan::EmptyRelation(EmptyRelation {
1827 produce_one_row: false,
1828 schema: Arc::new(input.schema().to_dfschema().unwrap()),
1829 });
1830 let logical_fold = HistogramFold::new(
1831 "le".to_string(),
1832 "val".to_string(),
1833 "host".to_string(),
1834 0.5,
1835 logical_input,
1836 )
1837 .unwrap();
1838 let constructed = logical_fold.to_execution_plan(input.clone());
1839 assert!(matches!(
1840 constructed.output_partitioning(),
1841 Partitioning::UnknownPartitioning(2)
1842 ));
1843
1844 let rebuilt = constructed
1845 .replace_children(
1846 vec![input],
1847 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1848 )
1849 .unwrap();
1850 assert!(matches!(
1851 rebuilt.output_partitioning(),
1852 Partitioning::UnknownPartitioning(2)
1853 ));
1854 }
1855
1856 #[test]
1857 fn confirm_schema() {
1858 let input_schema = Schema::new(vec![
1859 Field::new("host", DataType::Utf8, true),
1860 Field::new("le", DataType::Utf8, true),
1861 Field::new("val", DataType::Float64, true),
1862 ])
1863 .to_dfschema_ref()
1864 .unwrap();
1865 let expected_output_schema = Schema::new(vec![
1866 Field::new("host", DataType::Utf8, true),
1867 Field::new("val", DataType::Float64, true),
1868 ])
1869 .to_dfschema_ref()
1870 .unwrap();
1871
1872 let actual = HistogramFold::convert_schema(&input_schema, "le").unwrap();
1873 assert_eq!(actual, expected_output_schema)
1874 }
1875
1876 #[tokio::test]
1877 async fn fallback_to_safe_mode_on_missing_inf() {
1878 let schema = Arc::new(Schema::new(vec![
1879 Field::new("host", DataType::Utf8, true),
1880 Field::new("le", DataType::Utf8, true),
1881 Field::new("val", DataType::Float64, true),
1882 ]));
1883 let host_column = Arc::new(StringArray::from(vec!["a", "a", "b", "b", "c", "c"])) as _;
1884 let le_column = Arc::new(StringArray::from(vec![
1885 "0.1", "+Inf", "0.1", "1.0", "0.1", "+Inf",
1886 ])) as _;
1887 let val_column = Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 3.0, 1.0, 5.0])) as _;
1888 let batch =
1889 RecordBatch::try_new(schema.clone(), vec![host_column, le_column, val_column]).unwrap();
1890 let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
1891 let session_context = SessionContext::default();
1892 let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
1893 .await
1894 .unwrap();
1895 let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
1896 .unwrap()
1897 .to_string();
1898
1899 let expected = String::from(
1900 "+------+-----+
1901| host | val |
1902+------+-----+
1903| a | 0.1 |
1904| b | NaN |
1905| c | 0.1 |
1906+------+-----+",
1907 );
1908 assert_eq!(result_literal, expected);
1909 }
1910
1911 #[tokio::test]
1912 async fn emit_nan_when_no_inf_present() {
1913 let schema = Arc::new(Schema::new(vec![
1914 Field::new("host", DataType::Utf8, true),
1915 Field::new("le", DataType::Utf8, true),
1916 Field::new("val", DataType::Float64, true),
1917 ]));
1918 let host_column = Arc::new(StringArray::from(vec!["c", "c"])) as _;
1919 let le_column = Arc::new(StringArray::from(vec!["0.1", "1.0"])) as _;
1920 let val_column = Arc::new(Float64Array::from(vec![1.0, 2.0])) as _;
1921 let batch =
1922 RecordBatch::try_new(schema.clone(), vec![host_column, le_column, val_column]).unwrap();
1923 let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.9, 0);
1924 let session_context = SessionContext::default();
1925 let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
1926 .await
1927 .unwrap();
1928 let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
1929 .unwrap()
1930 .to_string();
1931
1932 let expected = String::from(
1933 "+------+-----+
1934| host | val |
1935+------+-----+
1936| c | NaN |
1937+------+-----+",
1938 );
1939 assert_eq!(result_literal, expected);
1940 }
1941
1942 #[tokio::test]
1943 async fn ignore_unparsable_bucket_bounds() {
1944 let schema = Arc::new(Schema::new(vec![
1945 Field::new("host", DataType::Utf8, true),
1946 Field::new("le", DataType::Utf8, true),
1947 Field::new("val", DataType::Float64, true),
1948 ]));
1949 let batch = RecordBatch::try_new(
1950 schema.clone(),
1951 vec![
1952 Arc::new(StringArray::from(vec!["a", "a", "a", "b"])),
1953 Arc::new(StringArray::from(vec![
1954 Some("bad"),
1955 Some("1"),
1956 Some("+Inf"),
1957 None,
1958 ])),
1959 Arc::new(Float64Array::from(vec![1.0, 2.0, 4.0, 1.0])),
1960 ],
1961 )
1962 .unwrap();
1963 let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
1964
1965 let result =
1966 datafusion::physical_plan::collect(fold_exec, SessionContext::default().task_ctx())
1967 .await
1968 .unwrap();
1969
1970 assert_eq!(result.len(), 1);
1971 assert_eq!(result[0].num_rows(), 1);
1972 assert_eq!(
1973 string_array_value_at_index(result[0].column(0), 0),
1974 Some("a")
1975 );
1976 assert_eq!(
1977 result[0].column(1).as_primitive::<Float64Type>().value(0),
1978 1.0
1979 );
1980 }
1981
1982 #[tokio::test]
1983 async fn trailing_invalid_bounds_fall_back_before_eof() {
1984 let schema = Arc::new(Schema::new(vec![
1985 Field::new("host", DataType::Utf8, false),
1986 Field::new("le", DataType::Utf8, true),
1987 Field::new("val", DataType::Float64, false),
1988 ]));
1989 for trailing_bound in [Some("bad"), None] {
1990 let batch = RecordBatch::try_new(
1991 schema.clone(),
1992 vec![
1993 Arc::new(StringArray::from(vec![
1994 "a", "a", "a", "b", "b", "b", "c", "c", "c",
1995 ])),
1996 Arc::new(StringArray::from(
1997 [Some("1"), Some("+Inf"), trailing_bound].repeat(3),
1998 )),
1999 Arc::new(Float64Array::from([2.0, 4.0, 99.0].repeat(3))),
2000 ],
2001 )
2002 .unwrap();
2003 let fold = build_fold_exec_from_batches(vec![batch.clone()], schema.clone(), 0.5, 0);
2004 let mut stream = HistogramFoldStream {
2005 le_column_index: 1,
2006 field_column_index: 2,
2007 histogram_column_index: None,
2008 operation: fold.operation,
2009 normal_indices: vec![0],
2010 bucket_size: None,
2011 batch_size: 1,
2012 output_schema: fold.output_schema.clone(),
2013 input_schema: schema.clone(),
2014 mode: FoldMode::Optimistic,
2015 safe_group: None,
2016 input_buffer: vec![],
2017 input_buffered_rows: 0,
2018 output_buffer: HistogramFoldStream::empty_output_buffer(&schema).unwrap(),
2019 output_buffered_rows: 0,
2020 input: fold
2021 .input
2022 .execute(0, SessionContext::default().task_ctx())
2023 .unwrap(),
2024 metric: BaselineMetrics::new(&fold.metric, 0),
2025 };
2026
2027 assert!(stream.fold_input(batch.slice(0, 2)).unwrap().is_none());
2029 assert!(stream.fold_input(batch.slice(2, 1)).unwrap().is_none());
2030 assert_eq!(stream.mode, FoldMode::Optimistic);
2031
2032 for (offset, host) in [(3, "a"), (6, "b")] {
2033 let output = stream
2034 .fold_input(batch.slice(offset, 3))
2035 .unwrap()
2036 .expect("a completed group should produce output before EOF")
2037 .unwrap();
2038 assert_eq!(stream.mode, FoldMode::Safe);
2039 assert_eq!(stream.input_buffered_rows, 0);
2040 assert!(stream.input_buffer.is_empty());
2041 assert_eq!(output.num_rows(), 1);
2042 assert_eq!(string_array_value_at_index(output.column(0), 0), Some(host));
2043 assert_eq!(output.column(1).as_primitive::<Float64Type>().value(0), 1.0);
2044 }
2045
2046 stream.flush_remaining().unwrap();
2047 let output = stream.take_output_buf().unwrap().unwrap();
2048 assert_eq!(output.num_rows(), 1);
2049 assert_eq!(string_array_value_at_index(output.column(0), 0), Some("c"));
2050 assert_eq!(output.column(1).as_primitive::<Float64Type>().value(0), 1.0);
2051 assert!(stream.take_output_buf().unwrap().is_none());
2052 }
2053 }
2054
2055 #[tokio::test]
2056 async fn safe_mode_handles_misaligned_groups() {
2057 let schema = Arc::new(Schema::new(vec![
2058 Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
2059 Field::new("le", DataType::Utf8, true),
2060 Field::new("val", DataType::Float64, true),
2061 ]));
2062
2063 let ts_column = Arc::new(TimestampMillisecondArray::from(vec![
2064 2900000, 2900000, 2900000, 3000000, 3000000, 3000000, 3000000, 3005000, 3005000,
2065 3010000, 3010000, 3010000, 3010000, 3010000,
2066 ])) as _;
2067 let le_column = Arc::new(StringArray::from(vec![
2068 "0.1", "1", "5", "0.1", "1", "5", "+Inf", "0.1", "+Inf", "0.1", "1", "3", "5", "+Inf",
2069 ])) as _;
2070 let val_column = Arc::new(Float64Array::from(vec![
2071 0.0, 0.0, 0.0, 50.0, 70.0, 110.0, 120.0, 10.0, 30.0, 10.0, 20.0, 30.0, 40.0, 50.0,
2072 ])) as _;
2073 let batch =
2074 RecordBatch::try_new(schema.clone(), vec![ts_column, le_column, val_column]).unwrap();
2075 let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
2076 let session_context = SessionContext::default();
2077 let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
2078 .await
2079 .unwrap();
2080
2081 let mut values = Vec::new();
2082 for batch in result {
2083 let array = batch.column(1).as_primitive::<Float64Type>();
2084 values.extend(array.iter().map(|v| v.unwrap()));
2085 }
2086
2087 assert_eq!(values.len(), 4);
2088 assert!(values[0].is_nan());
2089 assert!((values[1] - 0.55).abs() < 1e-10);
2090 assert!((values[2] - 0.1).abs() < 1e-10);
2091 assert!((values[3] - 2.0).abs() < 1e-10);
2092 }
2093
2094 #[tokio::test]
2095 async fn missing_buckets_at_first_timestamp() {
2096 let schema = Arc::new(Schema::new(vec![
2097 Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
2098 Field::new("le", DataType::Utf8, true),
2099 Field::new("val", DataType::Float64, true),
2100 ]));
2101
2102 let ts_column = Arc::new(TimestampMillisecondArray::from(vec![
2103 2_900_000, 3_000_000, 3_000_000, 3_000_000, 3_000_000, 3_005_000, 3_005_000, 3_010_000,
2104 3_010_000, 3_010_000, 3_010_000, 3_010_000,
2105 ])) as _;
2106 let le_column = Arc::new(StringArray::from(vec![
2107 "0.1", "0.1", "1", "5", "+Inf", "0.1", "+Inf", "0.1", "1", "3", "5", "+Inf",
2108 ])) as _;
2109 let val_column = Arc::new(Float64Array::from(vec![
2110 0.0, 50.0, 70.0, 110.0, 120.0, 10.0, 30.0, 10.0, 20.0, 30.0, 40.0, 50.0,
2111 ])) as _;
2112
2113 let batch =
2114 RecordBatch::try_new(schema.clone(), vec![ts_column, le_column, val_column]).unwrap();
2115 let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
2116 let session_context = SessionContext::default();
2117 let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
2118 .await
2119 .unwrap();
2120
2121 let mut values = Vec::new();
2122 for batch in result {
2123 let array = batch.column(1).as_primitive::<Float64Type>();
2124 values.extend(array.iter().map(|v| v.unwrap()));
2125 }
2126
2127 assert_eq!(values.len(), 4);
2128 assert!(values[0].is_nan());
2129 assert!((values[1] - 0.55).abs() < 1e-10);
2130 assert!((values[2] - 0.1).abs() < 1e-10);
2131 assert!((values[3] - 2.0).abs() < 1e-10);
2132 }
2133
2134 #[tokio::test]
2135 async fn missing_inf_in_first_group() {
2136 let schema = Arc::new(Schema::new(vec![
2137 Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
2138 Field::new("le", DataType::Utf8, true),
2139 Field::new("val", DataType::Float64, true),
2140 ]));
2141
2142 let ts_column = Arc::new(TimestampMillisecondArray::from(vec![
2143 1000, 1000, 1000, 2000, 2000, 2000, 2000,
2144 ])) as _;
2145 let le_column = Arc::new(StringArray::from(vec![
2146 "0.1", "1", "5", "0.1", "1", "5", "+Inf",
2147 ])) as _;
2148 let val_column = Arc::new(Float64Array::from(vec![
2149 0.0, 0.0, 0.0, 10.0, 20.0, 30.0, 30.0,
2150 ])) as _;
2151 let batch =
2152 RecordBatch::try_new(schema.clone(), vec![ts_column, le_column, val_column]).unwrap();
2153 let fold_exec = build_fold_exec_from_batches(vec![batch], schema, 0.5, 0);
2154 let session_context = SessionContext::default();
2155 let result = datafusion::physical_plan::collect(fold_exec, session_context.task_ctx())
2156 .await
2157 .unwrap();
2158
2159 let mut values = Vec::new();
2160 for batch in result {
2161 let array = batch.column(1).as_primitive::<Float64Type>();
2162 values.extend(array.iter().map(|v| v.unwrap()));
2163 }
2164
2165 assert_eq!(values.len(), 2);
2166 assert!(values[0].is_nan());
2167 assert!((values[1] - 0.55).abs() < 1e-10, "{values:?}");
2168 }
2169
2170 #[test]
2171 fn evaluate_quantile_duplicate_bounds() {
2172 let cases = [
2173 (
2175 vec![10.0, 10.0, 100.0, 100.0, 1000.0, f64::INFINITY],
2176 vec![40.0, 40.0, 50.0, 50.0, 50.0, 100.0],
2177 0.95,
2178 77.5,
2179 ),
2180 (
2182 vec![10.0, 10.0, 100.0, 100.0, f64::INFINITY],
2183 vec![30.0, 10.0, 40.0, 20.0, 60.0],
2184 0.5,
2185 7.5,
2186 ),
2187 (
2188 vec![10.0, f64::INFINITY, f64::INFINITY],
2189 vec![40.0, 20.0, 30.0],
2190 0.5,
2191 6.25,
2192 ),
2193 (
2194 vec![f64::INFINITY, f64::INFINITY],
2195 vec![20.0, 30.0],
2196 0.5,
2197 f64::NAN,
2198 ),
2199 (
2200 vec![-0.0, 0.0, 10.0, f64::INFINITY],
2201 vec![10.0, 10.0, 40.0, 40.0],
2202 0.75,
2203 5.0,
2204 ),
2205 ];
2206 for (bounds, counts, quantile, expected) in cases {
2207 let actual = HistogramFoldStream::evaluate_row(
2208 HistogramFoldOperation::Quantile(quantile.into()),
2209 &bounds,
2210 &counts,
2211 )
2212 .unwrap();
2213 if expected.is_nan() {
2214 assert!(actual.is_nan());
2215 } else {
2216 assert!((actual - expected).abs() < 1e-10, "{actual} != {expected}");
2217 }
2218 }
2219 }
2220
2221 #[tokio::test]
2222 async fn duplicate_infinity_bounds_across_batches() {
2223 let schema = Arc::new(Schema::new(vec![
2224 Field::new(
2225 "ts",
2226 DataType::Timestamp(TimeUnit::Millisecond, None),
2227 false,
2228 ),
2229 Field::new("le", DataType::Utf8, false),
2230 Field::new("val", DataType::Float64, false),
2231 ]));
2232 let duplicate = [("10", 20.0), ("10.0", 20.0), ("+Inf", 25.0), ("+inf", 25.0)];
2233 let regular = [("10", 20.0), ("10.0", 20.0), ("+Inf", 50.0)];
2234 for groups in [
2237 vec![duplicate.as_slice()],
2238 vec![duplicate.as_slice(), regular.as_slice()],
2239 vec![regular.as_slice(), duplicate.as_slice(), regular.as_slice()],
2240 ] {
2241 let mut timestamps = Vec::new();
2242 let mut bounds = Vec::new();
2243 let mut counts = Vec::new();
2244 for (timestamp, group) in groups.iter().enumerate() {
2245 for &(bound, count) in *group {
2246 timestamps.push(timestamp as i64);
2247 bounds.push(bound);
2248 counts.push(count);
2249 }
2250 }
2251 let batch = RecordBatch::try_new(
2252 schema.clone(),
2253 vec![
2254 Arc::new(TimestampMillisecondArray::from(timestamps)),
2255 Arc::new(StringArray::from(bounds)),
2256 Arc::new(Float64Array::from(counts)),
2257 ],
2258 )
2259 .unwrap();
2260 for batch_size in 1..=batch.num_rows() {
2261 let batches = (0..batch.num_rows())
2262 .step_by(batch_size)
2263 .map(|offset| batch.slice(offset, batch_size.min(batch.num_rows() - offset)))
2264 .collect();
2265 let fold = build_fold_exec_from_batches(batches, schema.clone(), 0.5, 0);
2266 let result =
2267 datafusion::physical_plan::collect(fold, SessionContext::default().task_ctx())
2268 .await
2269 .unwrap();
2270 let values: Vec<_> = result
2271 .iter()
2272 .flat_map(|batch| {
2273 batch
2274 .column(1)
2275 .as_primitive::<Float64Type>()
2276 .values()
2277 .iter()
2278 .copied()
2279 })
2280 .collect();
2281 assert_eq!(values, vec![6.25; groups.len()], "batch size {batch_size}");
2282 }
2283 }
2284 }
2285
2286 #[tokio::test]
2287 async fn duplicate_bounds_across_batches_and_safe_fallback() {
2288 let schema = Arc::new(Schema::new(vec![
2289 Field::new(
2290 "ts",
2291 DataType::Timestamp(TimeUnit::Millisecond, None),
2292 false,
2293 ),
2294 Field::new("le", DataType::Utf8, false),
2295 Field::new("val", DataType::Float64, false),
2296 ]));
2297 let batch = RecordBatch::try_new(
2300 schema.clone(),
2301 vec![
2302 Arc::new(TimestampMillisecondArray::from(vec![
2303 1000, 1000, 1000, 1000, 1000, 1000, 2000, 2000, 2000, 2000, 2000,
2304 ])),
2305 Arc::new(StringArray::from(vec![
2306 "10", "10.0", "100", "100.0", "1000", "+Inf", "10", "10.0", "100", "100.0",
2307 "+Inf",
2308 ])),
2309 Arc::new(Float64Array::from(vec![
2310 40.0, 40.0, 50.0, 50.0, 50.0, 100.0, 40.0, 40.0, 50.0, 50.0, 100.0,
2311 ])),
2312 ],
2313 )
2314 .unwrap();
2315 let batches = (0..batch.num_rows()).map(|i| batch.slice(i, 1)).collect();
2317 let fold = build_fold_exec_from_batches(batches, schema, 0.95, 0);
2318 let result = datafusion::physical_plan::collect(fold, SessionContext::default().task_ctx())
2319 .await
2320 .unwrap();
2321 let values: Vec<_> = result
2322 .iter()
2323 .flat_map(|batch| {
2324 batch
2325 .column(1)
2326 .as_primitive::<Float64Type>()
2327 .values()
2328 .iter()
2329 .copied()
2330 })
2331 .collect();
2332 assert_eq!(values, vec![77.5, 77.5]);
2333 }
2334
2335 #[test]
2336 fn evaluate_row_normal_case() {
2337 let bucket = [0.0, 1.0, 2.0, 3.0, 4.0, f64::INFINITY];
2338
2339 #[derive(Debug)]
2340 struct Case {
2341 quantile: f64,
2342 counters: Vec<f64>,
2343 expected: f64,
2344 }
2345
2346 let cases = [
2347 Case {
2348 quantile: 0.9,
2349 counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2350 expected: 4.0,
2351 },
2352 Case {
2353 quantile: 0.89,
2354 counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2355 expected: 4.0,
2356 },
2357 Case {
2358 quantile: 0.78,
2359 counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2360 expected: 3.9,
2361 },
2362 Case {
2363 quantile: 0.5,
2364 counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2365 expected: 2.5,
2366 },
2367 Case {
2368 quantile: 0.5,
2369 counters: vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
2370 expected: f64::NAN,
2371 },
2372 Case {
2373 quantile: 1.0,
2374 counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2375 expected: 4.0,
2376 },
2377 Case {
2378 quantile: 0.0,
2379 counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2380 expected: f64::NAN,
2381 },
2382 Case {
2383 quantile: 1.1,
2384 counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2385 expected: f64::INFINITY,
2386 },
2387 Case {
2388 quantile: -1.0,
2389 counters: vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
2390 expected: f64::NEG_INFINITY,
2391 },
2392 ];
2393
2394 for case in cases {
2395 let actual = HistogramFoldStream::evaluate_row(
2396 HistogramFoldOperation::Quantile(case.quantile.into()),
2397 &bucket,
2398 &case.counters,
2399 )
2400 .unwrap();
2401 assert_eq!(
2402 format!("{actual}"),
2403 format!("{}", case.expected),
2404 "{:?}",
2405 case
2406 );
2407 }
2408 }
2409
2410 #[test]
2411 fn evaluate_out_of_order_input() {
2412 let bucket = [0.0, 1.0, 2.0, 3.0, 4.0, f64::INFINITY];
2413 let counters = [5.0, 4.0, 3.0, 2.0, 1.0, 0.0];
2414 let result = HistogramFoldStream::evaluate_row(
2415 HistogramFoldOperation::Quantile(0.5.into()),
2416 &bucket,
2417 &counters,
2418 )
2419 .unwrap();
2420 assert_eq!(0.0, result);
2421 }
2422
2423 #[test]
2424 fn evaluate_wrong_bucket() {
2425 let bucket = [0.0, 1.0, 2.0, 3.0, 4.0, f64::INFINITY, 5.0];
2426 let counters = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
2427 let result = HistogramFoldStream::evaluate_row(
2428 HistogramFoldOperation::Quantile(0.5.into()),
2429 &bucket,
2430 &counters,
2431 );
2432 assert!(result.is_err());
2433 }
2434
2435 #[test]
2436 fn evaluate_small_fraction() {
2437 let bucket = [0.0, 2.0, 4.0, 6.0, f64::INFINITY];
2438 let counters = [0.0, 1.0 / 300.0, 2.0 / 300.0, 0.01, 0.01];
2439 let result = HistogramFoldStream::evaluate_row(
2440 HistogramFoldOperation::Quantile(0.5.into()),
2441 &bucket,
2442 &counters,
2443 )
2444 .unwrap();
2445 assert_eq!(3.0, result);
2446 }
2447
2448 #[test]
2449 fn evaluate_non_monotonic_counter() {
2450 let bucket = [0.0, 1.0, 2.0, 3.0, f64::INFINITY];
2451 let counters = [0.1, 0.2, 0.4, 0.17, 0.5];
2452 let result = HistogramFoldStream::evaluate_row(
2453 HistogramFoldOperation::Quantile(0.5.into()),
2454 &bucket,
2455 &counters,
2456 )
2457 .unwrap();
2458 assert!((result - 1.25).abs() < 1e-10, "{result}");
2459 }
2460
2461 #[test]
2462 fn evaluate_nan_counter() {
2463 let bucket = [0.0, 1.0, 2.0, 3.0, f64::INFINITY];
2464 let counters = [f64::NAN, 1.0, 2.0, 3.0, 3.0];
2465 let result = HistogramFoldStream::evaluate_row(
2466 HistogramFoldOperation::Quantile(0.5.into()),
2467 &bucket,
2468 &counters,
2469 )
2470 .unwrap();
2471 assert!((result - 1.5).abs() < 1e-10, "{result}");
2472 }
2473
2474 #[test]
2475 fn evaluate_classic_histogram_fraction() {
2476 let buckets = [1.0, 2.0, f64::INFINITY];
2477 let counters = [2.0, 4.0, 4.0];
2478 let fraction = |lower, upper| {
2479 HistogramFoldStream::evaluate_row(
2480 HistogramFoldOperation::Fraction {
2481 lower: OrderedF64::from(lower),
2482 upper: OrderedF64::from(upper),
2483 },
2484 &buckets,
2485 &counters,
2486 )
2487 .unwrap()
2488 };
2489
2490 assert_eq!(fraction(0.0, 1.0), 0.5);
2491 assert_eq!(fraction(f64::NEG_INFINITY, f64::INFINITY), 1.0);
2492 assert_eq!(fraction(2.0, 1.0), 0.0);
2493
2494 assert_eq!(
2495 HistogramFoldStream::evaluate_row(
2496 HistogramFoldOperation::Fraction {
2497 lower: 0.0.into(),
2498 upper: 1.0.into(),
2499 },
2500 &[1.0, 1.0, f64::INFINITY],
2501 &[1.0, 2.0, 4.0],
2502 )
2503 .unwrap(),
2504 0.75
2505 );
2506
2507 assert_eq!(
2508 HistogramFoldStream::evaluate_row(
2509 HistogramFoldOperation::Fraction {
2510 lower: f64::NEG_INFINITY.into(),
2511 upper: f64::INFINITY.into(),
2512 },
2513 &[f64::INFINITY],
2514 &[4.0],
2515 )
2516 .unwrap(),
2517 1.0
2518 );
2519 }
2520
2521 #[tokio::test]
2522 async fn fraction_handles_single_inf_bucket_after_safe_fallback() {
2523 let schema = Arc::new(Schema::new(vec![
2524 Field::new("host", DataType::Utf8, false),
2525 Field::new("le", DataType::Utf8, false),
2526 Field::new("val", DataType::Float64, false),
2527 ]));
2528 let batch = RecordBatch::try_new(
2529 schema.clone(),
2530 vec![
2531 Arc::new(StringArray::from(vec!["a", "a", "b"])),
2532 Arc::new(StringArray::from(vec!["1", "+Inf", "+Inf"])),
2533 Arc::new(Float64Array::from(vec![2.0, 4.0, 4.0])),
2534 ],
2535 )
2536 .unwrap();
2537 let fold = build_fold_exec_from_batches_with_operation(
2538 vec![batch],
2539 schema,
2540 HistogramFoldOperation::Fraction {
2541 lower: f64::NEG_INFINITY.into(),
2542 upper: f64::INFINITY.into(),
2543 },
2544 0,
2545 );
2546
2547 let batches =
2548 datafusion::physical_plan::collect(fold, SessionContext::default().task_ctx())
2549 .await
2550 .unwrap();
2551 let values = batches[0].column(1).as_primitive::<Float64Type>();
2552 assert_eq!(values.values(), &[1.0, 1.0]);
2553 }
2554
2555 fn build_empty_relation(schema: &Arc<Schema>) -> LogicalPlan {
2556 LogicalPlan::EmptyRelation(EmptyRelation {
2557 produce_one_row: false,
2558 schema: schema.clone().to_dfschema_ref().unwrap(),
2559 })
2560 }
2561
2562 #[tokio::test]
2563 async fn encode_decode_histogram_fold() {
2564 let schema = Arc::new(Schema::new(vec![
2565 Field::new("ts", DataType::Int64, false),
2566 Field::new("le", DataType::Utf8, false),
2567 Field::new("val", DataType::Float64, false),
2568 ]));
2569 let input_plan = build_empty_relation(&schema);
2570 let plan_node = HistogramFold::new(
2571 "le".to_string(),
2572 "val".to_string(),
2573 "ts".to_string(),
2574 0.8,
2575 input_plan.clone(),
2576 )
2577 .unwrap();
2578 let fraction_node = HistogramFold::new_with_operation(
2579 "le".to_string(),
2580 "val".to_string(),
2581 "ts".to_string(),
2582 HistogramFoldOperation::Fraction {
2583 lower: 0.0.into(),
2584 upper: 1.0.into(),
2585 },
2586 None,
2587 input_plan.clone(),
2588 )
2589 .unwrap();
2590 assert!(fraction_node.serialize().is_err());
2591
2592 let bytes = plan_node.serialize().unwrap();
2593
2594 let histogram_fold = HistogramFold::deserialize(&bytes).unwrap();
2595 let histogram_fold = histogram_fold
2597 .with_exprs_and_inputs(vec![], vec![input_plan])
2598 .unwrap();
2599
2600 assert_eq!(histogram_fold.le_column, "le");
2601 assert_eq!(histogram_fold.ts_column, "ts");
2602 assert_eq!(histogram_fold.field_column, "val");
2603 assert_eq!(
2604 histogram_fold.operation,
2605 HistogramFoldOperation::Quantile(OrderedF64::from(0.8))
2606 );
2607 assert_eq!(histogram_fold.output_schema.fields().len(), 2);
2608 assert_eq!(histogram_fold.output_schema.field(0).name(), "ts");
2609 assert_eq!(histogram_fold.output_schema.field(1).name(), "val");
2610 }
2611}