mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-12 06:52:34 +00:00
fix(promql): align subqueries to the absolute evaluation grid (#9400)
* fix(promql): align subqueries to the absolute evaluation grid Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * docs(promql): streamline subquery comments Remove redundant arithmetic narration and restated test comments. Keep grid, fallback, validation and overflow contracts, and preserve all regression tests and assertions. Regenerate SQLness output only for comment echoes. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --------- Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
This commit is contained in:
@@ -142,6 +142,16 @@ pub enum Error {
|
||||
location: Location,
|
||||
},
|
||||
|
||||
#[snafu(display(
|
||||
"The subquery child window is outside the representable millisecond range: {}",
|
||||
timestamp
|
||||
))]
|
||||
SubqueryTimestampOutOfRange {
|
||||
timestamp: String,
|
||||
#[snafu(implicit)]
|
||||
location: Location,
|
||||
},
|
||||
|
||||
#[snafu(display("Cannot find column {col}"))]
|
||||
ColumnNotFound {
|
||||
col: String,
|
||||
@@ -244,6 +254,7 @@ impl ErrorExt for Error {
|
||||
| ExpectRangeSelector { .. }
|
||||
| ZeroRangeSelector { .. }
|
||||
| InvalidTimeRange { .. }
|
||||
| SubqueryTimestampOutOfRange { .. }
|
||||
| ColumnNotFound { .. }
|
||||
| FunctionInvalidArgument { .. }
|
||||
| UnsupportedVectorMatch { .. }
|
||||
|
||||
@@ -48,7 +48,7 @@ use datafusion::functions_window::row_number::RowNumber;
|
||||
use datafusion::logical_expr::expr::{Alias, ScalarFunction, WindowFunction};
|
||||
use datafusion::logical_expr::expr_rewriter::normalize_cols;
|
||||
use datafusion::logical_expr::{
|
||||
BinaryExpr, Cast, Extension, LogicalPlan, LogicalPlanBuilder, Operator,
|
||||
BinaryExpr, Cast, EmptyRelation, Extension, LogicalPlan, LogicalPlanBuilder, Operator,
|
||||
ScalarUDF as ScalarUdfDef, WindowFrame, WindowFunctionDefinition,
|
||||
};
|
||||
use datafusion::prelude as df_prelude;
|
||||
@@ -107,9 +107,10 @@ use crate::promql::error::{
|
||||
CatalogSnafu, ColumnNotFoundSnafu, DataFusionPlanningSnafu, ExpectRangeSelectorSnafu,
|
||||
FunctionInvalidArgumentSnafu, InvalidDestinationLabelNameSnafu, InvalidRegularExpressionSnafu,
|
||||
InvalidTimeRangeSnafu, MultiFieldsNotSupportedSnafu, MultipleMetricMatchersSnafu,
|
||||
MultipleVectorSnafu, NoMetricMatcherSnafu, Result, TableNameNotFoundSnafu,
|
||||
TimeIndexNotFoundSnafu, UnexpectedPlanExprSnafu, UnexpectedTokenSnafu, UnknownTableSnafu,
|
||||
UnsupportedExprSnafu, UnsupportedMatcherOpSnafu, ValueNotFoundSnafu, ZeroRangeSelectorSnafu,
|
||||
MultipleVectorSnafu, NoMetricMatcherSnafu, Result, SubqueryTimestampOutOfRangeSnafu,
|
||||
TableNameNotFoundSnafu, TimeIndexNotFoundSnafu, UnexpectedPlanExprSnafu, UnexpectedTokenSnafu,
|
||||
UnknownTableSnafu, UnsupportedExprSnafu, UnsupportedMatcherOpSnafu, ValueNotFoundSnafu,
|
||||
ZeroRangeSelectorSnafu,
|
||||
};
|
||||
use crate::query_engine::QueryEngineState;
|
||||
|
||||
@@ -410,9 +411,7 @@ impl PromPlanner {
|
||||
..
|
||||
} = subquery_expr;
|
||||
|
||||
// Prometheus evaluates the inner expression over `(start - offset - range, end - offset]`
|
||||
// (`subqueryTimeRange`). Shift the inner window back here; `RangeManipulate` maps the
|
||||
// samples forward again by the same offset.
|
||||
// Shift the child window back; `RangeManipulate` restores the outer timeline.
|
||||
let offset_ms = match offset {
|
||||
Some(Offset::Pos(duration)) => duration.as_millis() as Millisecond,
|
||||
Some(Offset::Neg(duration)) => -(duration.as_millis() as Millisecond),
|
||||
@@ -423,14 +422,42 @@ impl PromPlanner {
|
||||
if let Some(step) = step {
|
||||
self.ctx.interval = step.as_millis() as _;
|
||||
}
|
||||
ensure!(self.ctx.interval > 0, ZeroRangeSelectorSnafu);
|
||||
let current_start = self.ctx.start;
|
||||
let current_end = self.ctx.end;
|
||||
self.ctx.start -= offset_ms + range.as_millis() as i64 - self.ctx.interval;
|
||||
self.ctx.end -= offset_ms;
|
||||
// Reject an invalid parent window rather than treating it as empty.
|
||||
ensure!(
|
||||
current_start <= current_end,
|
||||
InvalidTimeRangeSnafu {
|
||||
start: current_start,
|
||||
end: current_end,
|
||||
}
|
||||
);
|
||||
// An empty child needs only its schema; plan it over the valid caller window.
|
||||
let child_window = Self::subquery_child_window(
|
||||
current_start,
|
||||
current_end,
|
||||
current_interval,
|
||||
offset_ms,
|
||||
range.as_millis() as Millisecond,
|
||||
self.ctx.interval,
|
||||
)?;
|
||||
if let Some((child_start, child_end)) = child_window {
|
||||
self.ctx.start = child_start;
|
||||
self.ctx.end = child_end;
|
||||
}
|
||||
let input = self.prom_expr_to_plan(expr, query_engine_state).await?;
|
||||
self.ctx.interval = current_interval;
|
||||
self.ctx.start = current_start;
|
||||
self.ctx.end = current_end;
|
||||
// Return no rows with the child schema so enclosing fallbacks still work.
|
||||
let input = match child_window {
|
||||
Some(_) => input,
|
||||
None => LogicalPlan::EmptyRelation(EmptyRelation {
|
||||
produce_one_row: false,
|
||||
schema: input.schema().clone(),
|
||||
}),
|
||||
};
|
||||
|
||||
ensure!(!range.is_zero(), ZeroRangeSelectorSnafu);
|
||||
let range_ms = range.as_millis() as _;
|
||||
@@ -1656,6 +1683,47 @@ impl PromPlanner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the first absolute step multiple strictly after `start - offset - range`
|
||||
/// through the last parent evaluation instant minus `offset`. Requires a positive step.
|
||||
/// Returns `None` for an empty window; an unrepresentable nonempty bound is an error.
|
||||
fn subquery_child_window(
|
||||
start: Millisecond,
|
||||
end: Millisecond,
|
||||
interval: Millisecond,
|
||||
offset: Millisecond,
|
||||
range: Millisecond,
|
||||
step: Millisecond,
|
||||
) -> Result<Option<(Millisecond, Millisecond)>> {
|
||||
let window_start = i128::from(start) - i128::from(offset) - i128::from(range);
|
||||
let grid_start = (window_start.div_euclid(i128::from(step)) + 1) * i128::from(step);
|
||||
let grid_end = if interval > 0 {
|
||||
let steps = (i128::from(end) - i128::from(start)) / i128::from(interval);
|
||||
i128::from(start) + steps * i128::from(interval)
|
||||
} else {
|
||||
i128::from(end)
|
||||
} - i128::from(offset);
|
||||
// Check emptiness before converting bounds to i64.
|
||||
if grid_start > grid_end {
|
||||
return Ok(None);
|
||||
}
|
||||
let bound = |name: &str, value: i128| {
|
||||
i64::try_from(value).map_err(|_| {
|
||||
SubqueryTimestampOutOfRangeSnafu {
|
||||
timestamp: format!(
|
||||
"subquery child window {name} = {value} ms (start = {start} ms, \
|
||||
end = {end} ms, interval = {interval} ms, offset = {offset} ms, \
|
||||
range = {range} ms, step = {step} ms)"
|
||||
),
|
||||
}
|
||||
.build()
|
||||
})
|
||||
};
|
||||
Ok(Some((
|
||||
bound("grid start", grid_start)?,
|
||||
bound("grid end", grid_end)?,
|
||||
)))
|
||||
}
|
||||
|
||||
/// The columns that identify one series, which is the series key expected by the PromQL plan
|
||||
/// nodes that hold exactly one series per input batch.
|
||||
fn series_key_columns(&self) -> Vec<String> {
|
||||
|
||||
@@ -5134,6 +5134,605 @@ async fn count_over_time_subquery_with_offset() {
|
||||
indie_query_plan_compare(query, expected).await;
|
||||
}
|
||||
|
||||
/// Time-varying samples on and off the subquery step grid expose incorrect window folding.
|
||||
async fn build_subquery_offset_probe_provider() -> DfTableSourceProvider {
|
||||
let catalog_list = MemoryCatalogManager::with_default_setup();
|
||||
let probe_rows = (0..=6_i64)
|
||||
.map(|step| (step * 10_000, (step + 1) as f64))
|
||||
.collect::<Vec<_>>();
|
||||
let offgrid_rows = (0..6_i64)
|
||||
.map(|step| (step * 10_000 + 5_000, (step + 1) as f64))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (table_id, name, rows) in [
|
||||
(7_001_u32, "subquery_offset_probe", &probe_rows),
|
||||
(7_002_u32, "subquery_offset_offgrid", &offgrid_rows),
|
||||
] {
|
||||
let schema = Arc::new(Schema::new(vec![
|
||||
ColumnSchema::new(
|
||||
"host".to_string(),
|
||||
ConcreteDataType::string_datatype(),
|
||||
false,
|
||||
),
|
||||
ColumnSchema::new(
|
||||
"timestamp".to_string(),
|
||||
ConcreteDataType::timestamp_millisecond_datatype(),
|
||||
false,
|
||||
)
|
||||
.with_time_index(true),
|
||||
ColumnSchema::new(
|
||||
"val".to_string(),
|
||||
ConcreteDataType::float64_datatype(),
|
||||
true,
|
||||
),
|
||||
]));
|
||||
let batch = RecordBatch::try_new(
|
||||
schema.arrow_schema().clone(),
|
||||
vec![
|
||||
Arc::new(StringArray::from(vec!["a"; rows.len()])),
|
||||
Arc::new(TimestampMillisecondArray::from_iter_values(
|
||||
rows.iter().map(|(ts, _)| *ts),
|
||||
)),
|
||||
Arc::new(Float64Array::from_iter_values(
|
||||
rows.iter().map(|(_, value)| *value),
|
||||
)),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let backing = GreptimeMemTable::new_with_catalog(
|
||||
name,
|
||||
GreptimeRecordBatch::from_df_record_batch(schema.clone(), batch),
|
||||
table_id,
|
||||
DEFAULT_CATALOG_NAME.to_string(),
|
||||
DEFAULT_SCHEMA_NAME.to_string(),
|
||||
);
|
||||
let table_meta = TableMetaBuilder::empty()
|
||||
.schema(schema)
|
||||
.primary_key_indices(vec![0])
|
||||
.value_indices(vec![2])
|
||||
.next_column_id(3)
|
||||
.build()
|
||||
.unwrap();
|
||||
let table_info = Arc::new(
|
||||
TableInfoBuilder::default()
|
||||
.table_id(table_id)
|
||||
.name(name)
|
||||
.meta(table_meta)
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
let table = Arc::new(Table::new(
|
||||
table_info,
|
||||
FilterPushDownType::Unsupported,
|
||||
backing.data_source(),
|
||||
));
|
||||
assert!(
|
||||
catalog_list
|
||||
.register_table_sync(RegisterTableRequest {
|
||||
catalog: DEFAULT_CATALOG_NAME.to_string(),
|
||||
schema: DEFAULT_SCHEMA_NAME.to_string(),
|
||||
table_name: name.to_string(),
|
||||
table_id,
|
||||
table,
|
||||
})
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
DfTableSourceProvider::new(
|
||||
catalog_list,
|
||||
false,
|
||||
QueryContext::arc(),
|
||||
DummyDecoder::arc(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Plans on a seconds-based evaluation grid.
|
||||
async fn plan_subquery_offset_probe(
|
||||
query: &str,
|
||||
start: u64,
|
||||
end: u64,
|
||||
step: u64,
|
||||
) -> Result<LogicalPlan> {
|
||||
let eval_stmt = EvalStmt {
|
||||
expr: parser::parse(query).unwrap(),
|
||||
start: UNIX_EPOCH + Duration::from_secs(start),
|
||||
end: UNIX_EPOCH + Duration::from_secs(end),
|
||||
interval: Duration::from_secs(step),
|
||||
lookback_delta: Duration::from_secs(300),
|
||||
};
|
||||
PromPlanner::stmt_to_plan(
|
||||
build_subquery_offset_probe_provider().await,
|
||||
&eval_stmt,
|
||||
&build_query_engine_state(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Evaluates seconds-based bounds and returns sorted `(timestamp_ms, value)` rows.
|
||||
async fn run_subquery_offset_probe(
|
||||
query: &str,
|
||||
start: u64,
|
||||
end: u64,
|
||||
step: u64,
|
||||
) -> Vec<(i64, f64)> {
|
||||
let state = build_query_engine_state();
|
||||
let plan = plan_subquery_offset_probe(query, start, end, step)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (_, batches) = execute(plan, &state).await;
|
||||
let mut rows = Vec::new();
|
||||
for batch in &batches {
|
||||
let timestamp_index = batch
|
||||
.schema()
|
||||
.fields()
|
||||
.iter()
|
||||
.position(|field| matches!(field.data_type(), ArrowDataType::Timestamp(..)))
|
||||
.expect("no timestamp column");
|
||||
let value_index = batch
|
||||
.schema()
|
||||
.fields()
|
||||
.iter()
|
||||
.position(|field| field.data_type() == &ArrowDataType::Float64)
|
||||
.expect("no Float64 value column");
|
||||
let timestamps = batch
|
||||
.column(timestamp_index)
|
||||
.as_any()
|
||||
.downcast_ref::<TimestampMillisecondArray>()
|
||||
.expect("timestamp column is not a millisecond timestamp");
|
||||
let values = batch
|
||||
.column(value_index)
|
||||
.as_any()
|
||||
.downcast_ref::<Float64Array>()
|
||||
.expect("value column is not Float64");
|
||||
rows.extend(
|
||||
timestamps
|
||||
.iter()
|
||||
.zip(values.iter())
|
||||
.map(|(timestamp, value)| (timestamp.unwrap(), value.unwrap())),
|
||||
);
|
||||
}
|
||||
rows.sort_by_key(|(timestamp, _)| *timestamp);
|
||||
rows
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subquery_own_offset_shifts_the_inner_window_but_keeps_evaluation_timestamps() {
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_probe[20s:10s] offset 30s)",
|
||||
45,
|
||||
45,
|
||||
1
|
||||
)
|
||||
.await,
|
||||
vec![(45_000, 3.0)],
|
||||
);
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe("sum_over_time(subquery_offset_probe[20s:10s])", 15, 15, 1).await,
|
||||
vec![(15_000, 3.0)],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_probe[20s:10s] offset 30s)",
|
||||
30,
|
||||
60,
|
||||
30
|
||||
)
|
||||
.await,
|
||||
vec![(30_000, 1.0), (60_000, 7.0)],
|
||||
);
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe("sum_over_time(subquery_offset_probe[20s:10s])", 0, 30, 30).await,
|
||||
vec![(0, 1.0), (30_000, 7.0)],
|
||||
);
|
||||
|
||||
// Negative offsets look ahead while retaining outer timestamps.
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_probe[20s:10s] offset -30s)",
|
||||
0,
|
||||
30,
|
||||
30
|
||||
)
|
||||
.await,
|
||||
vec![(0, 7.0), (30_000, 13.0)],
|
||||
);
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe("sum_over_time(subquery_offset_probe[20s:10s])", 30, 60, 30)
|
||||
.await,
|
||||
vec![(30_000, 7.0), (60_000, 13.0)],
|
||||
);
|
||||
}
|
||||
|
||||
/// Expectations match Prometheus v3.14.0.
|
||||
#[tokio::test]
|
||||
async fn subquery_inner_points_are_anchored_on_absolute_step_multiples() {
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe("sum_over_time(subquery_offset_offgrid[20s:10s])", 45, 45, 1)
|
||||
.await,
|
||||
vec![(45_000, 7.0)],
|
||||
);
|
||||
|
||||
// An off-grid offset distinguishes absolute anchoring from re-phasing.
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[20s:10s] offset 5s)",
|
||||
30,
|
||||
30,
|
||||
1
|
||||
)
|
||||
.await,
|
||||
vec![(30_000, 3.0)],
|
||||
);
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[20s:10s] offset 5s)",
|
||||
45,
|
||||
45,
|
||||
1
|
||||
)
|
||||
.await,
|
||||
vec![(45_000, 7.0)],
|
||||
);
|
||||
|
||||
// The window starts before the first sample.
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[20s:10s] offset 15s)",
|
||||
30,
|
||||
30,
|
||||
1
|
||||
)
|
||||
.await,
|
||||
vec![(30_000, 1.0)],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[20s:10s] offset -5s)",
|
||||
15,
|
||||
15,
|
||||
1
|
||||
)
|
||||
.await,
|
||||
vec![(15_000, 3.0)],
|
||||
);
|
||||
|
||||
// The negative grid start exercises Euclidean alignment.
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe("sum_over_time(subquery_offset_offgrid[40s:10s])", 15, 15, 1)
|
||||
.await,
|
||||
vec![(15_000, 1.0)],
|
||||
);
|
||||
|
||||
// Multi-step evaluation starts off the subquery grid.
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[20s:10s])",
|
||||
35,
|
||||
65,
|
||||
30
|
||||
)
|
||||
.await,
|
||||
vec![(35_000, 5.0), (65_000, 11.0)],
|
||||
);
|
||||
|
||||
// Child points reach no sample at the first evaluation.
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[20s:10s] offset 30s)",
|
||||
30,
|
||||
60,
|
||||
30
|
||||
)
|
||||
.await,
|
||||
vec![(60_000, 5.0)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subquery_child_window_is_the_child_grid_window() {
|
||||
let child_window = PromPlanner::subquery_child_window;
|
||||
// Exclusive lower bound; upper bound is the last parent evaluation step.
|
||||
assert_eq!(
|
||||
child_window(30_000, 60_000, 10_000, 0, 20_000, 10_000).unwrap(),
|
||||
Some((20_000, 60_000))
|
||||
);
|
||||
assert_eq!(
|
||||
child_window(45_000, 60_000, 10_000, 0, 20_000, 10_000).unwrap(),
|
||||
Some((30_000, 55_000))
|
||||
);
|
||||
assert_eq!(
|
||||
child_window(25_000, 60_000, 10_000, 0, 20_000, 10_000).unwrap(),
|
||||
Some((10_000, 55_000))
|
||||
);
|
||||
assert_eq!(
|
||||
child_window(35_000, 65_000, 30_000, 0, 20_000, 10_000).unwrap(),
|
||||
Some((20_000, 65_000))
|
||||
);
|
||||
assert_eq!(
|
||||
child_window(0, 55_000, 30_000, 0, 20_000, 10_000).unwrap(),
|
||||
Some((-10_000, 30_000))
|
||||
);
|
||||
// A nonempty child window can contribute no parent rows.
|
||||
assert_eq!(
|
||||
child_window(35_000, 65_000, 10_000, 0, 5_000, 10_000).unwrap(),
|
||||
Some((40_000, 65_000))
|
||||
);
|
||||
// The first child point is past the parent’s last evaluation.
|
||||
assert_eq!(
|
||||
child_window(35_000, 44_000, 10_000, 0, 5_000, 10_000).unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
child_window(30_000, 30_000, 1_000, 5_000, 20_000, 10_000).unwrap(),
|
||||
Some((10_000, 25_000))
|
||||
);
|
||||
assert_eq!(
|
||||
child_window(15_000, 15_000, 1_000, -5_000, 20_000, 10_000).unwrap(),
|
||||
Some((10_000, 20_000))
|
||||
);
|
||||
// Boundaries before the epoch floor towards minus infinity.
|
||||
assert_eq!(
|
||||
child_window(-15_000, -15_000, 1_000, 0, 20_000, 10_000).unwrap(),
|
||||
Some((-30_000, -15_000))
|
||||
);
|
||||
assert_eq!(
|
||||
child_window(-45_000, -45_000, 1_000, -10_000, 20_000, 10_000).unwrap(),
|
||||
Some((-50_000, -35_000))
|
||||
);
|
||||
// A grid start before the first sample is not clamped to it.
|
||||
assert_eq!(
|
||||
child_window(0, 0, 1_000, 0, 20_000, 10_000).unwrap(),
|
||||
Some((-10_000, 0))
|
||||
);
|
||||
// A zero parent interval leaves the upper bound unaligned.
|
||||
assert_eq!(
|
||||
child_window(0, 55_000, 0, 0, 20_000, 10_000).unwrap(),
|
||||
Some((-10_000, 55_000))
|
||||
);
|
||||
|
||||
// Only the derived start overflows.
|
||||
let err = child_window(i64::MIN + 1_000, i64::MIN + 1_000, 1_000, 0, 2_000, 10).unwrap_err();
|
||||
assert!(
|
||||
matches!(
|
||||
&err,
|
||||
crate::promql::error::Error::SubqueryTimestampOutOfRange { timestamp, .. }
|
||||
if timestamp.contains("grid start") && timestamp.contains("-9223372036854776800")
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
assert_eq!(err.status_code(), StatusCode::InvalidArguments, "{err}");
|
||||
// Only the derived end overflows.
|
||||
let err = child_window(i64::MAX - 1_000, i64::MAX - 1_000, 1, -2_000, 3_000, 10).unwrap_err();
|
||||
assert!(
|
||||
matches!(
|
||||
&err,
|
||||
crate::promql::error::Error::SubqueryTimestampOutOfRange { timestamp, .. }
|
||||
if timestamp.contains("grid end") && timestamp.contains("9223372036854776807")
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
assert_eq!(err.status_code(), StatusCode::InvalidArguments, "{err}");
|
||||
// The wide window is nonempty even though its start overflows.
|
||||
let err = child_window(i64::MIN, i64::MAX, 10_000, i64::MAX, i64::MAX, 1_000).unwrap_err();
|
||||
assert!(
|
||||
matches!(
|
||||
&err,
|
||||
crate::promql::error::Error::SubqueryTimestampOutOfRange { timestamp, .. }
|
||||
if timestamp.contains("grid start")
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
// Both derived bounds overflow; report the exact start without clipping.
|
||||
let err = child_window(i64::MAX - 1_000, i64::MAX - 1_000, 1, i64::MIN, 10, 1).unwrap_err();
|
||||
assert!(
|
||||
matches!(
|
||||
&err,
|
||||
crate::promql::error::Error::SubqueryTimestampOutOfRange { timestamp, .. }
|
||||
if timestamp.contains("grid start")
|
||||
&& timestamp.contains("18446744073709550606")
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
assert_eq!(err.status_code(), StatusCode::InvalidArguments, "{err}");
|
||||
// Check emptiness before converting overflowing bounds.
|
||||
assert_eq!(
|
||||
child_window(i64::MAX, i64::MAX, 1_000, i64::MIN, 0, 1_000).unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subquery_child_window_without_a_step_reports_no_row() {
|
||||
// The first child point is after the parent’s only evaluation.
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe("sum_over_time(subquery_offset_offgrid[5s:10s])", 35, 44, 10)
|
||||
.await,
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[5s:10s]) or vector(1)",
|
||||
35,
|
||||
44,
|
||||
10
|
||||
)
|
||||
.await,
|
||||
vec![(35_000, 1.0)],
|
||||
);
|
||||
// Multi-step: every step of the grid reports no child point and falls back.
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[5s:10s]) or vector(1)",
|
||||
35,
|
||||
65,
|
||||
10
|
||||
)
|
||||
.await,
|
||||
vec![(35_000, 1.0), (45_000, 1.0), (55_000, 1.0), (65_000, 1.0)],
|
||||
);
|
||||
// A nonempty child grid can yield no parent rows; a longer range does.
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe("sum_over_time(subquery_offset_offgrid[5s:10s])", 35, 65, 10)
|
||||
.await,
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[20s:10s])",
|
||||
35,
|
||||
65,
|
||||
10
|
||||
)
|
||||
.await,
|
||||
vec![(35_000, 5.0), (45_000, 7.0), (55_000, 9.0), (65_000, 11.0)],
|
||||
);
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[20s:10s] offset 5s)",
|
||||
35,
|
||||
65,
|
||||
10
|
||||
)
|
||||
.await,
|
||||
vec![(35_000, 5.0), (45_000, 7.0), (55_000, 9.0), (65_000, 11.0)],
|
||||
);
|
||||
// A negative offset makes the short window reach a child point.
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[5s:10s] offset -5s)",
|
||||
35,
|
||||
44,
|
||||
10
|
||||
)
|
||||
.await,
|
||||
vec![(35_000, 4.0)],
|
||||
);
|
||||
|
||||
// An empty outer window suppresses inner results, but not an outer fallback.
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time((sum_over_time(subquery_offset_offgrid[5s:10s]))[5s:10s])",
|
||||
35,
|
||||
44,
|
||||
10
|
||||
)
|
||||
.await,
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time((sum_over_time(subquery_offset_offgrid[5s:10s]))[5s:10s]) or vector(1)",
|
||||
35,
|
||||
44,
|
||||
10
|
||||
)
|
||||
.await,
|
||||
vec![(35_000, 1.0)],
|
||||
);
|
||||
assert_eq!(
|
||||
run_subquery_offset_probe(
|
||||
"sum_over_time((sum_over_time(subquery_offset_offgrid[5s:10s]) or vector(1))[5s:10s])",
|
||||
35,
|
||||
44,
|
||||
10
|
||||
)
|
||||
.await,
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subquery_rejects_invalid_range_and_zero_step() {
|
||||
let err =
|
||||
plan_subquery_offset_probe("sum_over_time(subquery_offset_offgrid[5s:10s])", 44, 35, 10)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(
|
||||
&err,
|
||||
crate::promql::error::Error::InvalidTimeRange {
|
||||
start: 44_000,
|
||||
end: 35_000,
|
||||
..
|
||||
}
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
let err = plan_subquery_offset_probe(
|
||||
"sum_over_time((sum_over_time(subquery_offset_offgrid[5s:10s]))[5s:10s])",
|
||||
44,
|
||||
35,
|
||||
10,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(err.status_code(), StatusCode::InvalidArguments, "{err}");
|
||||
|
||||
// `[5s:0]` parses to zero; planning rejects it.
|
||||
let err = plan_subquery_offset_probe("sum_over_time(subquery_offset_offgrid[5s:0])", 60, 60, 1)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(&err, crate::promql::error::Error::ZeroRangeSelector { .. }),
|
||||
"{err}"
|
||||
);
|
||||
// An omitted child step inherits the zero parent step.
|
||||
let err = plan_subquery_offset_probe("sum_over_time(subquery_offset_offgrid[5s:])", 60, 60, 0)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(&err, crate::promql::error::Error::ZeroRangeSelector { .. }),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Rejects nonempty child windows whose derived millisecond bounds are unrepresentable.
|
||||
#[tokio::test]
|
||||
async fn subquery_rejects_a_child_window_out_of_the_representable_range() {
|
||||
// Offset is i64::MAX milliseconds; only the derived start overflows.
|
||||
let err = plan_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[21366776s:10s] offset 292471208y21366775s807ms)",
|
||||
0,
|
||||
60,
|
||||
10,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(
|
||||
&err,
|
||||
crate::promql::error::Error::SubqueryTimestampOutOfRange { timestamp, .. }
|
||||
if timestamp.contains("grid start = -9223372058221550000")
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
assert_eq!(err.status_code(), StatusCode::InvalidArguments, "{err}");
|
||||
// Offset is -i64::MAX milliseconds; only the derived end overflows.
|
||||
let err = plan_subquery_offset_probe(
|
||||
"sum_over_time(subquery_offset_offgrid[20s:10s] offset -292471208y21366775s807ms)",
|
||||
0,
|
||||
60,
|
||||
10,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(
|
||||
&err,
|
||||
crate::promql::error::Error::SubqueryTimestampOutOfRange { timestamp, .. }
|
||||
if timestamp.contains("grid end = 9223372036854835807")
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
assert_eq!(err.status_code(), StatusCode::InvalidArguments, "{err}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hash_join() {
|
||||
let mut eval_stmt = EvalStmt {
|
||||
|
||||
@@ -159,7 +159,174 @@ tql eval (30, 30, '1s') predict_linear(subquery_offset_total[20s:10s] offset -30
|
||||
| 1970-01-01T00:00:30 | 4.0 | a |
|
||||
+---------------------+----------------------------------------------+------+
|
||||
|
||||
-- Offset results match the shifted evaluation while retaining outer timestamps.
|
||||
-- Verified against Prometheus v3.14.0.
|
||||
tql eval (30, 60, '30s') sum_over_time(subquery_offset_total[20s:10s] offset 30s);
|
||||
|
||||
+---------------------+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+---------------------+----------------------------------+------+
|
||||
| 1970-01-01T00:00:30 | 1.0 | a |
|
||||
| 1970-01-01T00:01:00 | 7.0 | a |
|
||||
+---------------------+----------------------------------+------+
|
||||
|
||||
tql eval (0, 30, '30s') sum_over_time(subquery_offset_total[20s:10s]);
|
||||
|
||||
+---------------------+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+---------------------+----------------------------------+------+
|
||||
| 1970-01-01T00:00:00 | 1.0 | a |
|
||||
| 1970-01-01T00:00:30 | 7.0 | a |
|
||||
+---------------------+----------------------------------+------+
|
||||
|
||||
drop table subquery_offset_total;
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
-- Off-grid samples expose absolute step anchoring.
|
||||
create table subquery_offset_offgrid (
|
||||
ts timestamp time index,
|
||||
host string primary key,
|
||||
val double,
|
||||
);
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
insert into subquery_offset_offgrid values
|
||||
(5000, 'a', 1),
|
||||
(15000, 'a', 2),
|
||||
(25000, 'a', 3),
|
||||
(35000, 'a', 4),
|
||||
(45000, 'a', 5),
|
||||
(55000, 'a', 6);
|
||||
|
||||
Affected Rows: 6
|
||||
|
||||
tql eval (30, 30, '1s') sum_over_time(subquery_offset_offgrid[20s:10s] offset 5s);
|
||||
|
||||
+---------------------+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+---------------------+----------------------------------+------+
|
||||
| 1970-01-01T00:00:30 | 3.0 | a |
|
||||
+---------------------+----------------------------------+------+
|
||||
|
||||
-- Offset moves the window without re-phasing its step points.
|
||||
tql eval (30, 30, '1s') sum_over_time(subquery_offset_offgrid[20s:10s]);
|
||||
|
||||
+---------------------+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+---------------------+----------------------------------+------+
|
||||
| 1970-01-01T00:00:30 | 5.0 | a |
|
||||
+---------------------+----------------------------------+------+
|
||||
|
||||
-- The window starts before the first sample.
|
||||
tql eval (30, 30, '1s') sum_over_time(subquery_offset_offgrid[20s:10s] offset 15s);
|
||||
|
||||
+---------------------+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+---------------------+----------------------------------+------+
|
||||
| 1970-01-01T00:00:30 | 1.0 | a |
|
||||
+---------------------+----------------------------------+------+
|
||||
|
||||
tql eval (15, 15, '1s') sum_over_time(subquery_offset_offgrid[20s:10s] offset -5s);
|
||||
|
||||
+---------------------+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+---------------------+----------------------------------+------+
|
||||
| 1970-01-01T00:00:15 | 3.0 | a |
|
||||
+---------------------+----------------------------------+------+
|
||||
|
||||
-- Multi-step evaluation uses the non-aligned parent timeline.
|
||||
tql eval (35, 65, '30s') sum_over_time(subquery_offset_offgrid[20s:10s]);
|
||||
|
||||
+---------------------+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+---------------------+----------------------------------+------+
|
||||
| 1970-01-01T00:00:35 | 5.0 | a |
|
||||
| 1970-01-01T00:01:05 | 11.0 | a |
|
||||
+---------------------+----------------------------------+------+
|
||||
|
||||
-- An evaluation instant whose window reaches no sample has no row (30s), unlike 60s -> 2 + 3
|
||||
tql eval (30, 60, '30s') sum_over_time(subquery_offset_offgrid[20s:10s] offset 30s);
|
||||
|
||||
+---------------------+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+---------------------+----------------------------------+------+
|
||||
| 1970-01-01T00:01:00 | 5.0 | a |
|
||||
+---------------------+----------------------------------+------+
|
||||
|
||||
-- No child point yields no row; the enclosing fallback still fires.
|
||||
tql eval (35, 44, '10s') sum_over_time(subquery_offset_offgrid[5s:10s]);
|
||||
|
||||
+----+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+----+----------------------------------+------+
|
||||
+----+----------------------------------+------+
|
||||
|
||||
tql eval (35, 44, '10s') sum_over_time(subquery_offset_offgrid[5s:10s]) or vector(1);
|
||||
|
||||
+---------------------+------+----------------------------------+
|
||||
| ts | host | prom_sum_over_time(ts_range,val) |
|
||||
+---------------------+------+----------------------------------+
|
||||
| 1970-01-01T00:00:35 | | 1.0 |
|
||||
+---------------------+------+----------------------------------+
|
||||
|
||||
-- A negative offset makes the short window reach a child point.
|
||||
tql eval (35, 44, '10s') sum_over_time(subquery_offset_offgrid[5s:10s] offset -5s);
|
||||
|
||||
+---------------------+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+---------------------+----------------------------------+------+
|
||||
| 1970-01-01T00:00:35 | 4.0 | a |
|
||||
+---------------------+----------------------------------+------+
|
||||
|
||||
-- An empty outer window suppresses inner results, but not an outer fallback.
|
||||
tql eval (35, 44, '10s') sum_over_time((sum_over_time(subquery_offset_offgrid[5s:10s]))[5s:10s]);
|
||||
|
||||
+----+---------------------------------------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,prom_sum_over_time(ts_range,val)) | host |
|
||||
+----+---------------------------------------------------------------+------+
|
||||
+----+---------------------------------------------------------------+------+
|
||||
|
||||
tql eval (35, 44, '10s') sum_over_time((sum_over_time(subquery_offset_offgrid[5s:10s]))[5s:10s]) or vector(1);
|
||||
|
||||
+---------------------+------+---------------------------------------------------------------+
|
||||
| ts | host | prom_sum_over_time(ts_range,prom_sum_over_time(ts_range,val)) |
|
||||
+---------------------+------+---------------------------------------------------------------+
|
||||
| 1970-01-01T00:00:35 | | 1.0 |
|
||||
+---------------------+------+---------------------------------------------------------------+
|
||||
|
||||
-- An aligned instant has a child point in the same short range.
|
||||
tql eval (30, 30, '1s') sum_over_time(subquery_offset_offgrid[5s:10s]);
|
||||
|
||||
+---------------------+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+---------------------+----------------------------------+------+
|
||||
| 1970-01-01T00:00:30 | 3.0 | a |
|
||||
+---------------------+----------------------------------+------+
|
||||
|
||||
-- A longer range covers child steps on the off-grid parent timeline.
|
||||
tql eval (35, 65, '10s') sum_over_time(subquery_offset_offgrid[20s:10s]);
|
||||
|
||||
+---------------------+----------------------------------+------+
|
||||
| ts | prom_sum_over_time(ts_range,val) | host |
|
||||
+---------------------+----------------------------------+------+
|
||||
| 1970-01-01T00:00:35 | 5.0 | a |
|
||||
| 1970-01-01T00:00:45 | 7.0 | a |
|
||||
| 1970-01-01T00:00:55 | 9.0 | a |
|
||||
| 1970-01-01T00:01:05 | 11.0 | a |
|
||||
+---------------------+----------------------------------+------+
|
||||
|
||||
-- Zero step parses here but is invalid for planning.
|
||||
tql eval (60, 60, '1s') sum_over_time(subquery_offset_offgrid[5s:0]);
|
||||
|
||||
Error: 1004(InvalidArguments), Zero range in range selector
|
||||
|
||||
tql eval (44, 35, '10s') sum_over_time(subquery_offset_offgrid[5s:10s]);
|
||||
|
||||
Error: 1004(InvalidArguments), The end time must be greater than start time, start: 44000, end: 35000
|
||||
|
||||
drop table subquery_offset_offgrid;
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
|
||||
@@ -65,4 +65,67 @@ tql eval (60, 60, '1s') predict_linear(subquery_offset_total[20s:10s] offset 30s
|
||||
|
||||
tql eval (30, 30, '1s') predict_linear(subquery_offset_total[20s:10s] offset -30s, 0);
|
||||
|
||||
-- Offset results match the shifted evaluation while retaining outer timestamps.
|
||||
-- Verified against Prometheus v3.14.0.
|
||||
tql eval (30, 60, '30s') sum_over_time(subquery_offset_total[20s:10s] offset 30s);
|
||||
|
||||
tql eval (0, 30, '30s') sum_over_time(subquery_offset_total[20s:10s]);
|
||||
|
||||
drop table subquery_offset_total;
|
||||
|
||||
-- Off-grid samples expose absolute step anchoring.
|
||||
create table subquery_offset_offgrid (
|
||||
ts timestamp time index,
|
||||
host string primary key,
|
||||
val double,
|
||||
);
|
||||
|
||||
insert into subquery_offset_offgrid values
|
||||
(5000, 'a', 1),
|
||||
(15000, 'a', 2),
|
||||
(25000, 'a', 3),
|
||||
(35000, 'a', 4),
|
||||
(45000, 'a', 5),
|
||||
(55000, 'a', 6);
|
||||
|
||||
tql eval (30, 30, '1s') sum_over_time(subquery_offset_offgrid[20s:10s] offset 5s);
|
||||
|
||||
-- Offset moves the window without re-phasing its step points.
|
||||
tql eval (30, 30, '1s') sum_over_time(subquery_offset_offgrid[20s:10s]);
|
||||
|
||||
-- The window starts before the first sample.
|
||||
tql eval (30, 30, '1s') sum_over_time(subquery_offset_offgrid[20s:10s] offset 15s);
|
||||
|
||||
tql eval (15, 15, '1s') sum_over_time(subquery_offset_offgrid[20s:10s] offset -5s);
|
||||
|
||||
-- Multi-step evaluation uses the non-aligned parent timeline.
|
||||
tql eval (35, 65, '30s') sum_over_time(subquery_offset_offgrid[20s:10s]);
|
||||
|
||||
-- An evaluation instant whose window reaches no sample has no row (30s), unlike 60s -> 2 + 3
|
||||
tql eval (30, 60, '30s') sum_over_time(subquery_offset_offgrid[20s:10s] offset 30s);
|
||||
|
||||
-- No child point yields no row; the enclosing fallback still fires.
|
||||
tql eval (35, 44, '10s') sum_over_time(subquery_offset_offgrid[5s:10s]);
|
||||
|
||||
tql eval (35, 44, '10s') sum_over_time(subquery_offset_offgrid[5s:10s]) or vector(1);
|
||||
|
||||
-- A negative offset makes the short window reach a child point.
|
||||
tql eval (35, 44, '10s') sum_over_time(subquery_offset_offgrid[5s:10s] offset -5s);
|
||||
|
||||
-- An empty outer window suppresses inner results, but not an outer fallback.
|
||||
tql eval (35, 44, '10s') sum_over_time((sum_over_time(subquery_offset_offgrid[5s:10s]))[5s:10s]);
|
||||
|
||||
tql eval (35, 44, '10s') sum_over_time((sum_over_time(subquery_offset_offgrid[5s:10s]))[5s:10s]) or vector(1);
|
||||
|
||||
-- An aligned instant has a child point in the same short range.
|
||||
tql eval (30, 30, '1s') sum_over_time(subquery_offset_offgrid[5s:10s]);
|
||||
|
||||
-- A longer range covers child steps on the off-grid parent timeline.
|
||||
tql eval (35, 65, '10s') sum_over_time(subquery_offset_offgrid[20s:10s]);
|
||||
|
||||
-- Zero step parses here but is invalid for planning.
|
||||
tql eval (60, 60, '1s') sum_over_time(subquery_offset_offgrid[5s:0]);
|
||||
|
||||
tql eval (44, 35, '10s') sum_over_time(subquery_offset_offgrid[5s:10s]);
|
||||
|
||||
drop table subquery_offset_offgrid;
|
||||
|
||||
Reference in New Issue
Block a user