feat(promql): support native histogram vector operators (#8798)

* feat(promql): support native histogram vector operators

Signed-off-by: shuiyisong <xixing.sys@gmail.com>

* fix: CR issue

Signed-off-by: shuiyisong <xixing.sys@gmail.com>

* chore: rebase main

Signed-off-by: shuiyisong <xixing.sys@gmail.com>

* fix: cr issue

Signed-off-by: shuiyisong <xixing.sys@gmail.com>

* fix: cr issue

Signed-off-by: shuiyisong <xixing.sys@gmail.com>

* fix: cr issue

Signed-off-by: shuiyisong <xixing.sys@gmail.com>

---------

Signed-off-by: shuiyisong <xixing.sys@gmail.com>
This commit is contained in:
shuiyisong
2026-08-11 16:01:06 +08:00
committed by GitHub
parent f27f27da66
commit 97cbf79fb6
3 changed files with 1668 additions and 165 deletions
@@ -18,6 +18,7 @@ use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use common_query::prelude::{greptime_native_histogram, greptime_value};
use datafusion::arrow::array::{Array, TimestampMillisecondArray, UInt64Array};
use datafusion::arrow::datatypes::{DataType, SchemaRef};
use datafusion::arrow::record_batch::RecordBatch;
@@ -52,6 +53,15 @@ use crate::metrics::PROMQL_SERIES_COUNT;
const MAX_INSTANT_MANIPULATE_OUTPUT_POINTS: usize = 1_000_000;
fn mixed_sample_fields(field: Option<&str>) -> [Option<&str>; 2] {
let companion = match field {
Some(field) if field == greptime_value() => Some(greptime_native_histogram()),
Some(field) if field == greptime_native_histogram() => Some(greptime_value()),
_ => None,
};
[field, companion]
}
/// Manipulate the input record batch to make it suitable for Instant Operator.
///
/// This plan will try to align the input time series, for every timestamp between
@@ -66,7 +76,7 @@ pub struct InstantManipulate {
time_index_column: String,
// Planner-provided tag-column hint for execution fast paths.
tag_columns: Vec<String>,
/// A optional column for validating staleness
/// Primary sample column used to derive the columns checked for staleness.
field_column: Option<String>,
input: LogicalPlan,
unfix: Option<UnfixIndices>,
@@ -97,9 +107,7 @@ impl UserDefinedLogicalNodeCore for InstantManipulate {
}
let mut exprs = vec![col(&self.time_index_column)];
if let Some(field) = &self.field_column {
exprs.push(col(field));
}
exprs.extend(self.staleness_field_columns().map(col));
exprs
}
@@ -116,7 +124,7 @@ impl UserDefinedLogicalNodeCore for InstantManipulate {
let mut required = output_columns.to_vec();
required.push(input_schema.index_of_column_by_name(None, &self.time_index_column)?);
if let Some(field) = &self.field_column {
for field in self.staleness_field_columns() {
required.push(input_schema.index_of_column_by_name(None, field)?);
}
@@ -223,6 +231,21 @@ impl InstantManipulate {
"InstantManipulate"
}
fn staleness_field_columns(&self) -> impl Iterator<Item = &str> {
let [field, companion] = mixed_sample_fields(self.field_column.as_deref());
[
field,
companion.filter(|companion| {
self.input
.schema()
.index_of_column_by_name(None, companion)
.is_some()
}),
]
.into_iter()
.flatten()
}
fn resolve_tag_columns(input: &LogicalPlan, tag_columns: &[String]) -> Vec<String> {
if !tag_columns.is_empty() {
return tag_columns.to_vec();
@@ -385,11 +408,9 @@ impl ExecutionPlan for InstantManipulateExec {
.column_with_name(&self.time_index_column)
.expect("time index column not found")
.0;
let field_index = self
.field_column
.as_ref()
.and_then(|name| schema.column_with_name(name))
.map(|x| x.0);
let field_indices = mixed_sample_fields(self.field_column.as_deref()).map(|field| {
field.and_then(|field| schema.column_with_name(field).map(|(index, _)| index))
});
let tsid_index = schema
.column_with_name("__tsid")
.filter(|(_, field)| field.data_type() == &DataType::UInt64)
@@ -400,7 +421,7 @@ impl ExecutionPlan for InstantManipulateExec {
lookback_delta: self.lookback_delta,
interval: self.interval,
time_index,
field_index,
field_indices,
tsid_index,
reuse_tsid_column: self.reuse_tsid_column && tsid_index.is_some(),
schema,
@@ -467,7 +488,7 @@ pub struct InstantManipulateStream {
interval: Millisecond,
// Column index of TIME INDEX column's position in schema
time_index: usize,
field_index: Option<usize>,
field_indices: [Option<usize>; 2],
tsid_index: Option<usize>,
reuse_tsid_column: bool,
@@ -532,11 +553,16 @@ impl InstantManipulateStream {
return Ok(input);
}
// Field column for staleness checks, classified once per batch.
let stale_sample_column = self
.field_index
.map(|index| input.column(index).as_ref())
.and_then(prometheus_stale_sample_column);
// Field columns for staleness checks, classified once per batch.
let stale_sample_columns = self.field_indices.map(|index| {
index.and_then(|index| prometheus_stale_sample_column(input.column(index).as_ref()))
});
let is_stale = |row| {
stale_sample_columns
.iter()
.flatten()
.any(|column| is_prometheus_stale_sample(*column, row))
};
// Optimize iteration range based on actual data bounds
let first_ts = ts_column.value(0);
@@ -581,9 +607,7 @@ impl InstantManipulateStream {
let curr = ts_column.value(cursor);
match curr.cmp(&expected_ts) {
Ordering::Equal => {
if stale_sample_column
.is_some_and(|column| is_prometheus_stale_sample(column, cursor))
{
if is_stale(cursor) {
// Ignore the stale marker.
} else {
take_indices.push(cursor as u64);
@@ -615,9 +639,7 @@ impl InstantManipulateStream {
let prev_ts = ts_column.value(prev_cursor);
if prev_ts + self.lookback_delta > expected_ts {
// only use the point in the time range
if stale_sample_column
.is_some_and(|column| is_prometheus_stale_sample(column, prev_cursor))
{
if is_stale(prev_cursor) {
// Do not use a stale marker as the newest value.
continue;
}
@@ -626,9 +648,7 @@ impl InstantManipulateStream {
aligned_ts.push(expected_ts);
}
}
} else if stale_sample_column
.is_some_and(|column| is_prometheus_stale_sample(column, cursor))
{
} else if is_stale(cursor) {
// Do not use a stale marker as the newest value.
} else {
// use this point
+14 -6
View File
@@ -173,12 +173,20 @@ impl ScalarUDFImpl for NativeHistogramAnnotationUdf {
}
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> DfResult<ColumnarValue> {
if let Some(collector) = args
.config_options
.extensions
.get::<PromqlAnnotationCollector>()
.cloned()
.or_else(|| self.collector.clone())
let has_dropped_sample = !args.args.is_empty()
&& (0..args.number_rows).any(|row| {
args.args.iter().all(|arg| match arg {
ColumnarValue::Array(array) => array.is_valid(row),
ColumnarValue::Scalar(value) => !value.is_null(),
})
});
if has_dropped_sample
&& let Some(collector) = args
.config_options
.extensions
.get::<PromqlAnnotationCollector>()
.cloned()
.or_else(|| self.collector.clone())
{
collector.record_info(self.message.clone());
}
File diff suppressed because it is too large Load Diff