mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-12 23:12:28 +00:00
fix(promql): align timestamp(), label_join and label_replace with Prometheus semantics (#9385)
* fix(promql): align timestamp() and label_join with Prometheus semantics
- timestamp() over a selector reports the selected sample's timestamp, without adding the offset.
- timestamp() over any other expression reports the evaluation time instead of the input value.
- label_join that overwrites an existing label rejects duplicate label sets at runtime.
Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
* fix(promql): align label_replace and label_join edge cases with Prometheus
- label_replace leaves non-matching series unchanged instead of copying the source value into the destination label.
- label_replace may overwrite an existing label; duplicate label sets are rejected at runtime like label_join.
- label_join with no source labels removes the destination label, and an empty source label name is rejected.
- Empty label values produced by these functions are NULL, the same as an absent label.
Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
* fix(promql): join absent labels as empty strings in label_join
concat_ws skips NULL arguments together with their separator, so a label that is absent (NULL) dropped its separator, e.g. label_join(label_join(vector(1), "a", "", "missing"), "b", "-", "a", "a") lost b="-". Read every source label as coalesce(label, '') like label_replace does.
Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
* fix(promql): skip rows without a sample in timestamp()
timestamp() replaced the input fields with the timestamp before the empty-value filter ran, so a row whose fields were all NULL got a timestamp, e.g. timestamp(-m) for a NULL sample. Filter such rows before the projection, for both the selector and the evaluation-time paths.
Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
---------
Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
(cherry picked from commit 78a7b93292)
Signed-off-by: WenyXu <wenymedia@gmail.com>
This commit is contained in:
@@ -34,6 +34,8 @@ pub enum MatchGroupViolation {
|
||||
ImplicitManyToOne,
|
||||
/// A group modifier left several matches with the same result label set.
|
||||
AmbiguousGroupLabels,
|
||||
/// A function rewrote labels so that several series share the same label set.
|
||||
DuplicateLabelSet,
|
||||
}
|
||||
|
||||
impl MatchGroupViolation {
|
||||
@@ -54,6 +56,9 @@ impl MatchGroupViolation {
|
||||
Self::AmbiguousGroupLabels => format!(
|
||||
"multiple matches for labels {group}: grouping labels must ensure unique matches"
|
||||
),
|
||||
Self::DuplicateLabelSet => {
|
||||
"vector cannot contain metrics with the same labelset".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,12 +217,6 @@ pub enum Error {
|
||||
location: Location,
|
||||
},
|
||||
|
||||
#[snafu(display("vector cannot contain metrics with the same labelset"))]
|
||||
SameLabelSet {
|
||||
#[snafu(implicit)]
|
||||
location: Location,
|
||||
},
|
||||
|
||||
#[snafu(display("Invalid regular expression in label_replace(): {}", regex))]
|
||||
InvalidRegularExpression {
|
||||
regex: String,
|
||||
@@ -256,7 +250,6 @@ impl ErrorExt for Error {
|
||||
| CombineTableColumnMismatch { .. }
|
||||
| UnexpectedPlanExpr { .. }
|
||||
| UnsupportedMatcherOp { .. }
|
||||
| SameLabelSet { .. }
|
||||
| TimestampOutOfRange { .. }
|
||||
| SystemTimeOutOfRange { .. }
|
||||
| AtModifierTimestampOutOfRange { .. }
|
||||
|
||||
+167
-138
@@ -107,7 +107,7 @@ use crate::promql::error::{
|
||||
CatalogSnafu, ColumnNotFoundSnafu, DataFusionPlanningSnafu, ExpectRangeSelectorSnafu,
|
||||
FunctionInvalidArgumentSnafu, InvalidDestinationLabelNameSnafu, InvalidRegularExpressionSnafu,
|
||||
InvalidTimeRangeSnafu, MultiFieldsNotSupportedSnafu, MultipleMetricMatchersSnafu,
|
||||
MultipleVectorSnafu, NoMetricMatcherSnafu, Result, SameLabelSetSnafu, TableNameNotFoundSnafu,
|
||||
MultipleVectorSnafu, NoMetricMatcherSnafu, Result, TableNameNotFoundSnafu,
|
||||
TimeIndexNotFoundSnafu, UnexpectedPlanExprSnafu, UnexpectedTokenSnafu, UnknownTableSnafu,
|
||||
UnsupportedExprSnafu, UnsupportedMatcherOpSnafu, ValueNotFoundSnafu, ZeroRangeSelectorSnafu,
|
||||
};
|
||||
@@ -1752,47 +1752,9 @@ impl PromPlanner {
|
||||
DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// `timestamp()` preserves the shifted selector timeline even though
|
||||
// SeriesNormalize now retains raw native timestamp storage. Decimal
|
||||
// arithmetic shifts before truncating to milliseconds.
|
||||
let unit_factor = match ident(&time_index_column)
|
||||
.get_type(normalize.schema())
|
||||
.context(DataFusionPlanningSnafu)?
|
||||
{
|
||||
ArrowDataType::Timestamp(ArrowTimeUnit::Second, _) => (1_000_i128, 4, 0),
|
||||
ArrowDataType::Timestamp(ArrowTimeUnit::Millisecond, _) => (1, 1, 0),
|
||||
ArrowDataType::Timestamp(ArrowTimeUnit::Microsecond, _) => (1, 4, 3),
|
||||
ArrowDataType::Timestamp(ArrowTimeUnit::Nanosecond, _) => (1, 7, 6),
|
||||
_ => unreachable!("time index is a timestamp"),
|
||||
};
|
||||
let sample_time = ident(&time_index_column)
|
||||
.cast_to(&ArrowDataType::Int64, normalize.schema())
|
||||
.context(DataFusionPlanningSnafu)?
|
||||
.cast_to(&ArrowDataType::Decimal128(19, 0), normalize.schema())
|
||||
.context(DataFusionPlanningSnafu)?;
|
||||
let sample_time = DfExpr::BinaryExpr(BinaryExpr {
|
||||
left: Box::new(sample_time),
|
||||
op: Operator::Multiply,
|
||||
right: Box::new(lit(ScalarValue::Decimal128(
|
||||
Some(unit_factor.0),
|
||||
unit_factor.1,
|
||||
unit_factor.2,
|
||||
))),
|
||||
});
|
||||
let sample_time = DfExpr::BinaryExpr(BinaryExpr {
|
||||
left: Box::new(sample_time),
|
||||
op: Operator::Plus,
|
||||
right: Box::new(lit(ScalarValue::Decimal128(Some(offset_ms as i128), 19, 0))),
|
||||
})
|
||||
.cast_to(&ArrowDataType::Int64, normalize.schema())
|
||||
.context(DataFusionPlanningSnafu)?
|
||||
.cast_to(&ArrowDataType::Float64, normalize.schema())
|
||||
.context(DataFusionPlanningSnafu)?;
|
||||
let sample_time = DfExpr::BinaryExpr(BinaryExpr {
|
||||
left: Box::new(sample_time),
|
||||
op: Operator::Divide,
|
||||
right: Box::new(lit(1000.0)),
|
||||
});
|
||||
// The time index still holds the raw sample timestamp here, which is what
|
||||
// `timestamp()` reports regardless of `offset` and `@`.
|
||||
let sample_time = Self::timestamp_seconds_expr(&time_index_column, normalize.schema())?;
|
||||
project_exprs.push(sample_time.alias(×tamp_value_column));
|
||||
let normalize = LogicalPlanBuilder::from(normalize)
|
||||
.project(project_exprs)
|
||||
@@ -1846,40 +1808,51 @@ impl PromPlanner {
|
||||
}),
|
||||
};
|
||||
if let Some(timestamp_value_column) = timestamp_value_column {
|
||||
self.create_timestamp_func_plan(manipulate, ×tamp_value_column)
|
||||
self.create_timestamp_func_plan(manipulate, ident(timestamp_value_column))
|
||||
} else {
|
||||
Ok(manipulate)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a projection plan for the PromQL `timestamp()` function.
|
||||
/// Projects the time index column as the value column for each row.
|
||||
/// Converts the timestamp column `column` into PromQL seconds, truncated to milliseconds.
|
||||
fn timestamp_seconds_expr(column: &str, schema: &DFSchema) -> Result<DfExpr> {
|
||||
let column = DfExpr::Column(Column::from_name(column));
|
||||
let ArrowDataType::Timestamp(_, timezone) =
|
||||
column.get_type(schema).context(DataFusionPlanningSnafu)?
|
||||
else {
|
||||
unreachable!("time index is a timestamp")
|
||||
};
|
||||
let millis = column
|
||||
.cast_to(
|
||||
&ArrowDataType::Timestamp(ArrowTimeUnit::Millisecond, timezone),
|
||||
schema,
|
||||
)
|
||||
.context(DataFusionPlanningSnafu)?
|
||||
.cast_to(&ArrowDataType::Int64, schema)
|
||||
.context(DataFusionPlanningSnafu)?
|
||||
.cast_to(&ArrowDataType::Float64, schema)
|
||||
.context(DataFusionPlanningSnafu)?;
|
||||
Ok(DfExpr::BinaryExpr(BinaryExpr {
|
||||
left: Box::new(millis),
|
||||
op: Operator::Divide,
|
||||
right: Box::new(lit(1000.0)),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Builds a projection plan for the PromQL `timestamp()` function, which reports
|
||||
/// `timestamp_value` as the value of each row, along with the original tag and time index
|
||||
/// columns.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `input` - Input [`LogicalPlan`] after instant-vector selection.
|
||||
/// * `timestamp_value_column` - Private column containing each selected sample's timestamp.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns a [`Result<LogicalPlan>`] where the resulting logical plan projects the timestamp
|
||||
/// column as the value column, along with the original tag and time index columns.
|
||||
///
|
||||
/// # Timestamp vs. Time Function
|
||||
///
|
||||
/// - **Timestamp Function (`timestamp()`)**: In PromQL, the `timestamp()` function returns the
|
||||
/// timestamp (time index) of each sample as the value column.
|
||||
///
|
||||
/// - **Time Function (`time()`)**: The `time()` function returns the evaluation time of the query
|
||||
/// as a scalar value.
|
||||
///
|
||||
/// # Side Effects
|
||||
/// Updates the planner context's field columns to the timestamp column name.
|
||||
///
|
||||
fn create_timestamp_func_plan(
|
||||
&mut self,
|
||||
input: LogicalPlan,
|
||||
timestamp_value_column: &str,
|
||||
timestamp_value: DfExpr,
|
||||
) -> Result<LogicalPlan> {
|
||||
let time_expr = ident(timestamp_value_column).alias(DEFAULT_FIELD_COLUMN);
|
||||
// A row whose fields are all NULL holds no sample, so it must not get a timestamp. The
|
||||
// check reads the input fields, which the projection below replaces.
|
||||
let has_sample = self.create_empty_values_filter_expr(true)?;
|
||||
let time_expr = timestamp_value.alias(DEFAULT_FIELD_COLUMN);
|
||||
self.ctx.field_columns = vec![time_expr.schema_name().to_string()];
|
||||
let mut project_exprs = Vec::with_capacity(self.ctx.tag_columns.len() + 2);
|
||||
project_exprs.push(self.create_time_index_column_expr()?);
|
||||
@@ -1887,6 +1860,8 @@ impl PromPlanner {
|
||||
project_exprs.extend(self.create_tag_column_exprs()?);
|
||||
|
||||
LogicalPlanBuilder::from(input)
|
||||
.filter(has_sample)
|
||||
.context(DataFusionPlanningSnafu)?
|
||||
.project(project_exprs)
|
||||
.context(DataFusionPlanningSnafu)?
|
||||
.build()
|
||||
@@ -2029,9 +2004,31 @@ impl PromPlanner {
|
||||
|
||||
// transform function arguments
|
||||
let args = self.create_function_args(&args.args)?;
|
||||
// Only a vector selector keeps the timestamps of its samples. Any other expression
|
||||
// produces samples at the evaluation time, which is what `timestamp()` reports for it.
|
||||
let mut timestamp_arg = args.input.as_ref();
|
||||
while let Some(PromExpr::Paren(ParenExpr { expr })) = timestamp_arg {
|
||||
timestamp_arg = Some(expr);
|
||||
}
|
||||
let timestamp_of_selector =
|
||||
func.name == "timestamp" && matches!(timestamp_arg, Some(PromExpr::VectorSelector(_)));
|
||||
let input = if let Some(prom_expr) = &args.input {
|
||||
self.prom_expr_to_plan_inner(prom_expr, func.name == "timestamp", query_engine_state)
|
||||
.await?
|
||||
let input = self
|
||||
.prom_expr_to_plan_inner(prom_expr, timestamp_of_selector, query_engine_state)
|
||||
.await?;
|
||||
if func.name == "timestamp" && !timestamp_of_selector {
|
||||
let time_index_column =
|
||||
self.ctx
|
||||
.time_index_column
|
||||
.clone()
|
||||
.with_context(|| TimeIndexNotFoundSnafu {
|
||||
table: self.ctx.table_name.clone().unwrap_or_default(),
|
||||
})?;
|
||||
let eval_time = Self::timestamp_seconds_expr(&time_index_column, input.schema())?;
|
||||
self.create_timestamp_func_plan(input, eval_time)?
|
||||
} else {
|
||||
input
|
||||
}
|
||||
} else {
|
||||
self.ctx.time_index_column = Some(SPECIAL_TIME_FUNCTION.to_string());
|
||||
self.ctx.reset_table_name_and_schema();
|
||||
@@ -2057,6 +2054,7 @@ impl PromPlanner {
|
||||
// `create_function_expr`: the function that reads it (`predict_linear`) then depends on an
|
||||
// argument instead of on planner state written by the selector below it.
|
||||
let range_fold_offset = self.ctx.range_fold_offset.take();
|
||||
let input_tag_columns = self.ctx.tag_columns.clone();
|
||||
let (mut func_exprs, new_tags) = self.create_function_expr(
|
||||
func,
|
||||
args.literals.clone(),
|
||||
@@ -2108,13 +2106,31 @@ impl PromPlanner {
|
||||
_ => builder,
|
||||
};
|
||||
|
||||
// Rewriting a label the input series already have can map several of them onto the same
|
||||
// label set, which PromQL rejects. A new label keeps the series distinct.
|
||||
let may_duplicate_label_sets = matches!(func.name, "label_join" | "label_replace")
|
||||
&& new_tags.iter().any(|tag| input_tag_columns.contains(tag));
|
||||
|
||||
// Update context tags after building plan
|
||||
// We can't push them before planning, because they won't exist until projection.
|
||||
for tag in new_tags {
|
||||
self.ctx.tag_columns.push(tag);
|
||||
}
|
||||
|
||||
let plan = builder.build().context(DataFusionPlanningSnafu)?;
|
||||
let mut plan = builder.build().context(DataFusionPlanningSnafu)?;
|
||||
if may_duplicate_label_sets {
|
||||
let labels = self.ctx.tag_columns.clone();
|
||||
plan = Self::assert_unique_match_group(
|
||||
plan,
|
||||
labels
|
||||
.iter()
|
||||
.map(|label| DfExpr::Column(Column::from_name(label)))
|
||||
.collect(),
|
||||
labels,
|
||||
self.create_time_index_column_expr()?,
|
||||
MatchGroupViolation::DuplicateLabelSet,
|
||||
)?;
|
||||
}
|
||||
common_telemetry::debug!("Created PromQL function plan: {plan:?} for {call_expr:?}");
|
||||
|
||||
Ok(plan)
|
||||
@@ -3620,6 +3636,7 @@ impl PromPlanner {
|
||||
let (concat_expr, dst_label) = Self::build_concat_labels_expr(
|
||||
&mut other_input_exprs,
|
||||
&self.ctx,
|
||||
input_schema,
|
||||
query_engine_state,
|
||||
)?;
|
||||
|
||||
@@ -3641,8 +3658,8 @@ impl PromPlanner {
|
||||
}
|
||||
"label_replace" => {
|
||||
self.ctx.use_tsid = false;
|
||||
if let Some((replace_expr, dst_label)) = self
|
||||
.build_regexp_replace_label_expr(&mut other_input_exprs, query_engine_state)?
|
||||
if let Some((replace_expr, dst_label)) =
|
||||
self.build_regexp_replace_label_expr(&mut other_input_exprs, input_schema)?
|
||||
{
|
||||
// Reserve the current field columns except the `dst_label`.
|
||||
for value in &self.ctx.field_columns {
|
||||
@@ -3652,10 +3669,8 @@ impl PromPlanner {
|
||||
}
|
||||
}
|
||||
|
||||
ensure!(
|
||||
!self.ctx.tag_columns.contains(&dst_label),
|
||||
SameLabelSetSnafu
|
||||
);
|
||||
// Remove it from tag columns if exists to avoid duplicated column names
|
||||
self.ctx.tag_columns.retain(|tag| *tag != dst_label);
|
||||
new_tags.push(dst_label);
|
||||
// Add the new label expr to evaluate
|
||||
exprs.push(replace_expr);
|
||||
@@ -3964,7 +3979,7 @@ impl PromPlanner {
|
||||
fn build_regexp_replace_label_expr(
|
||||
&self,
|
||||
other_input_exprs: &mut VecDeque<DfExpr>,
|
||||
query_engine_state: &QueryEngineState,
|
||||
input_schema: &DFSchemaRef,
|
||||
) -> Result<Option<(DfExpr, String)>> {
|
||||
// label_replace(vector, dst_label, replacement, src_label, regex)
|
||||
let dst_label = match other_input_exprs.pop_front() {
|
||||
@@ -4000,72 +4015,64 @@ impl PromPlanner {
|
||||
.fail()?,
|
||||
};
|
||||
|
||||
// Validate the regex before using it
|
||||
// Like Prometheus, match the whole source value. A series whose source value matches gets
|
||||
// `dst_label` set to the expanded replacement; any other series is left unchanged.
|
||||
// doc: https://prometheus.io/docs/prometheus/latest/querying/functions/#label_replace
|
||||
regex::Regex::new(®ex).map_err(|_| {
|
||||
InvalidRegularExpressionSnafu {
|
||||
regex: regex.clone(),
|
||||
}
|
||||
.build()
|
||||
})?;
|
||||
let anchored = format!("^(?s:{regex})$");
|
||||
let compiled = regex::Regex::new(&anchored)
|
||||
.map_err(|_| InvalidRegularExpressionSnafu { regex }.build())?;
|
||||
let dst_exists = self.ctx.tag_columns.contains(&dst_label);
|
||||
|
||||
// If the src_label exists and regex is empty, keep everything unchanged.
|
||||
if self.ctx.tag_columns.contains(&src_label) && regex.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// If the src_label doesn't exists, and
|
||||
// A missing source label reads as the empty string for every series, so the result is
|
||||
// the same for all of them and is decided here.
|
||||
if !self.ctx.tag_columns.contains(&src_label) {
|
||||
if replacement.is_empty() {
|
||||
// the replacement is empty, keep everything unchanged.
|
||||
let Some(captures) = compiled.captures("") else {
|
||||
return Ok(None);
|
||||
} else {
|
||||
// the replacement is not empty, always adds dst_label with replacement value.
|
||||
};
|
||||
let mut value = String::new();
|
||||
captures.expand(&replacement, &mut value);
|
||||
if value.is_empty() {
|
||||
// Setting a label to the empty string removes it, which is a no-op for a new
|
||||
// label.
|
||||
if !dst_exists {
|
||||
return Ok(None);
|
||||
}
|
||||
return Ok(Some((
|
||||
// alias literal `replacement` as dst_label
|
||||
lit(replacement).alias(&dst_label),
|
||||
lit(ScalarValue::Utf8(None)).alias(&dst_label),
|
||||
dst_label,
|
||||
)));
|
||||
}
|
||||
return Ok(Some((lit(value).alias(&dst_label), dst_label)));
|
||||
}
|
||||
|
||||
// Preprocess the regex:
|
||||
// https://github.com/prometheus/prometheus/blob/d902abc50d6652ba8fe9a81ff8e5cce936114eba/promql/functions.go#L1575C32-L1575C37
|
||||
let regex = format!("^(?s:{regex})$");
|
||||
let src = Self::label_value_expr(&src_label, input_schema)?;
|
||||
let matched = DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: datafusion_functions::regex::regexp_like(),
|
||||
args: vec![src.clone(), lit(anchored.clone())],
|
||||
});
|
||||
let replaced = Self::empty_label_to_null(DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: datafusion_functions::regex::regexp_replace(),
|
||||
args: vec![src, lit(anchored), lit(replacement)],
|
||||
}));
|
||||
let unchanged = if dst_exists {
|
||||
DfExpr::Column(Column::from_name(&dst_label))
|
||||
.cast_to(&ArrowDataType::Utf8, input_schema)
|
||||
.context(DataFusionPlanningSnafu)?
|
||||
} else {
|
||||
lit(ScalarValue::Utf8(None))
|
||||
};
|
||||
let replace_expr = when(matched, replaced)
|
||||
.otherwise(unchanged)
|
||||
.context(DataFusionPlanningSnafu)?;
|
||||
|
||||
let session_state = query_engine_state.session_state();
|
||||
let func = session_state
|
||||
.scalar_functions()
|
||||
.get("regexp_replace")
|
||||
.context(UnsupportedExprSnafu {
|
||||
name: "regexp_replace",
|
||||
})?;
|
||||
|
||||
// regexp_replace(src_label, regex, replacement)
|
||||
let args = vec![
|
||||
if src_label.is_empty() {
|
||||
DfExpr::Literal(ScalarValue::Utf8(Some(String::new())), None)
|
||||
} else {
|
||||
DfExpr::Column(Column::from_name(src_label))
|
||||
},
|
||||
DfExpr::Literal(ScalarValue::Utf8(Some(regex)), None),
|
||||
DfExpr::Literal(ScalarValue::Utf8(Some(replacement)), None),
|
||||
];
|
||||
|
||||
Ok(Some((
|
||||
DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: func.clone(),
|
||||
args,
|
||||
})
|
||||
.alias(&dst_label),
|
||||
dst_label,
|
||||
)))
|
||||
Ok(Some((replace_expr.alias(&dst_label), dst_label)))
|
||||
}
|
||||
|
||||
/// Build expr for `label_join` function
|
||||
fn build_concat_labels_expr(
|
||||
other_input_exprs: &mut VecDeque<DfExpr>,
|
||||
ctx: &PromPlannerContext,
|
||||
input_schema: &DFSchemaRef,
|
||||
query_engine_state: &QueryEngineState,
|
||||
) -> Result<(DfExpr, String)> {
|
||||
// label_join(vector, dst_label, separator, src_label_1, src_label_2, ...)
|
||||
@@ -4097,17 +4104,19 @@ impl PromPlanner {
|
||||
let src_labels = other_input_exprs
|
||||
.iter()
|
||||
.map(|expr| {
|
||||
// Cast source label into column or null literal
|
||||
// `concat_ws` skips NULL arguments together with their separator, while an
|
||||
// absent label joins as the empty string.
|
||||
match expr {
|
||||
DfExpr::Literal(ScalarValue::Utf8(Some(label)), None) => {
|
||||
if label.is_empty() {
|
||||
Ok(DfExpr::Literal(ScalarValue::Null, None))
|
||||
FunctionInvalidArgumentSnafu {
|
||||
fn_name: "label_join",
|
||||
}
|
||||
.fail()
|
||||
} else if available_columns.contains(label.as_str()) {
|
||||
// Label exists in the table schema
|
||||
Ok(DfExpr::Column(Column::from_name(label)))
|
||||
Self::label_value_expr(label, input_schema)
|
||||
} else {
|
||||
// Label doesn't exist, treat as empty string (null)
|
||||
Ok(DfExpr::Literal(ScalarValue::Null, None))
|
||||
Ok(lit(""))
|
||||
}
|
||||
}
|
||||
other => UnexpectedPlanExprSnafu {
|
||||
@@ -4120,12 +4129,10 @@ impl PromPlanner {
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
ensure!(
|
||||
!src_labels.is_empty(),
|
||||
FunctionInvalidArgumentSnafu {
|
||||
fn_name: "label_join"
|
||||
}
|
||||
);
|
||||
// Joining no labels yields the empty string, i.e. removes `dst_label`.
|
||||
if src_labels.is_empty() {
|
||||
return Ok((lit(ScalarValue::Utf8(None)).alias(&dst_label), dst_label));
|
||||
}
|
||||
|
||||
let session_state = query_engine_state.session_state();
|
||||
let func = session_state
|
||||
@@ -4139,15 +4146,37 @@ impl PromPlanner {
|
||||
args.extend(src_labels);
|
||||
|
||||
Ok((
|
||||
DfExpr::ScalarFunction(ScalarFunction {
|
||||
Self::empty_label_to_null(DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: func.clone(),
|
||||
args,
|
||||
})
|
||||
}))
|
||||
.alias(&dst_label),
|
||||
dst_label,
|
||||
))
|
||||
}
|
||||
|
||||
/// The value of `label` as a string, where NULL (the series has no such label) reads as the
|
||||
/// empty string, as in PromQL.
|
||||
fn label_value_expr(label: &str, input_schema: &DFSchemaRef) -> Result<DfExpr> {
|
||||
let value = DfExpr::Column(Column::from_name(label))
|
||||
.cast_to(&ArrowDataType::Utf8, input_schema)
|
||||
.context(DataFusionPlanningSnafu)?;
|
||||
Ok(DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: coalesce(),
|
||||
args: vec![value, lit("")],
|
||||
}))
|
||||
}
|
||||
|
||||
/// An empty label value means the label is absent in PromQL. Label functions represent it
|
||||
/// as NULL, like a series that never had the label, so that both compare equal when label
|
||||
/// sets are matched and neither is reported as a label.
|
||||
fn empty_label_to_null(value: DfExpr) -> DfExpr {
|
||||
DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: datafusion_functions::core::nullif(),
|
||||
args: vec![value, lit("")],
|
||||
})
|
||||
}
|
||||
|
||||
fn create_time_index_column_expr(&self) -> Result<DfExpr> {
|
||||
Ok(DfExpr::Column(Column::from_name(
|
||||
self.ctx
|
||||
|
||||
@@ -1699,12 +1699,13 @@ async fn single_timestamp_plan_preserves_source_value() {
|
||||
"Filter: value IS NOT NULL [timestamp:Timestamp(ms), value:Float64, tag_0:Utf8]\
|
||||
\n Projection: some_metric.timestamp, value AS value, some_metric.tag_0 [timestamp:Timestamp(ms), value:Float64, tag_0:Utf8]\
|
||||
\n Projection: some_metric.timestamp, __promql_timestamp_value_ AS value, some_metric.tag_0 [timestamp:Timestamp(ms), value:Float64, tag_0:Utf8]\
|
||||
\n PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, __promql_timestamp_value_:Float64]\
|
||||
\n Projection: some_metric.tag_0, some_metric.timestamp, some_metric.field_0, CAST(CAST(CAST(CAST(some_metric.timestamp AS Int64) AS Decimal128(19, 0)) * Decimal128(1,1,0) + Decimal128(0,19,0) AS Int64) AS Float64) / Float64(1000) AS __promql_timestamp_value_ [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, __promql_timestamp_value_:Float64]\
|
||||
\n PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
|
||||
\n Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
|
||||
\n Filter: some_metric.tag_0 != Utf8(\"bar\") AND some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
|
||||
\n TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
|
||||
\n Filter: some_metric.field_0 IS NOT NULL [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, __promql_timestamp_value_:Float64]\
|
||||
\n PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, __promql_timestamp_value_:Float64]\
|
||||
\n Projection: some_metric.tag_0, some_metric.timestamp, some_metric.field_0, CAST(CAST(some_metric.timestamp AS Int64) AS Float64) / Float64(1000) AS __promql_timestamp_value_ [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N, __promql_timestamp_value_:Float64]\
|
||||
\n PromSeriesDivide: tags=[\"tag_0\"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
|
||||
\n Sort: some_metric.tag_0 ASC NULLS FIRST, some_metric.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
|
||||
\n Filter: some_metric.tag_0 != Utf8(\"bar\") AND some_metric.timestamp >= TimestampMillisecond(-999, None) AND some_metric.timestamp <= TimestampMillisecond(100000000, None) [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]\
|
||||
\n TableScan: some_metric [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]",
|
||||
);
|
||||
|
||||
assert_eq!(plan.display_indent_schema().to_string(), expected);
|
||||
@@ -2413,9 +2414,9 @@ async fn at_modifier_keeps_multi_series_roots_out_of_promoted_subtree() {
|
||||
async fn at_modifier_does_not_promote_label_join() {
|
||||
for query in [
|
||||
// Directly above the anchored instant selector...
|
||||
"label_join(some_metric @ 300, \"tag_0\", \"-\", \"\")",
|
||||
"label_join(some_metric @ 300, \"tag_0\", \"-\", \"tag_0\", \"tag_0\")",
|
||||
// ... and below another call, which is planned as usual over the join.
|
||||
"abs(label_join(some_metric @ 300, \"tag_0\", \"-\", \"\"))",
|
||||
"abs(label_join(some_metric @ 300, \"tag_0\", \"-\", \"tag_0\", \"tag_0\"))",
|
||||
] {
|
||||
let plan = build_at_modifier_plan(query, 0, 1000).await;
|
||||
let plan_str = plan.display_indent_schema().to_string();
|
||||
@@ -2448,7 +2449,7 @@ async fn at_modifier_does_not_promote_label_join() {
|
||||
// A range call below the join is still promoted on its own: the anchored window is folded
|
||||
// once per series, and the join above it is evaluated at every step over that replay.
|
||||
let plan = build_at_modifier_plan(
|
||||
"label_join(rate(some_metric[5m] @ 300), \"tag_0\", \"-\", \"\")",
|
||||
"label_join(rate(some_metric[5m] @ 300), \"tag_0\", \"-\", \"tag_0\", \"tag_0\")",
|
||||
0,
|
||||
1000,
|
||||
)
|
||||
@@ -5902,7 +5903,7 @@ async fn test_label_join() {
|
||||
|
||||
let expected = r#"
|
||||
Filter: up.field_0 IS NOT NULL [timestamp:Timestamp(ms), field_0:Float64;N, foo:Utf8;N, tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8]
|
||||
Projection: up.timestamp, up.field_0, concat_ws(Utf8(","), up.tag_1, up.tag_2, up.tag_3) AS foo, up.tag_0, up.tag_1, up.tag_2, up.tag_3 [timestamp:Timestamp(ms), field_0:Float64;N, foo:Utf8;N, tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8]
|
||||
Projection: up.timestamp, up.field_0, nullif(concat_ws(Utf8(","), coalesce(up.tag_1, Utf8("")), coalesce(up.tag_2, Utf8("")), coalesce(up.tag_3, Utf8(""))), Utf8("")) AS foo, up.tag_0, up.tag_1, up.tag_2, up.tag_3 [timestamp:Timestamp(ms), field_0:Float64;N, foo:Utf8;N, tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8]
|
||||
PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
|
||||
PromSeriesDivide: tags=["tag_0", "tag_1", "tag_2", "tag_3"] [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
|
||||
Sort: up.tag_0 ASC NULLS FIRST, up.tag_1 ASC NULLS FIRST, up.tag_2 ASC NULLS FIRST, up.tag_3 ASC NULLS FIRST, up.timestamp ASC NULLS FIRST [tag_0:Utf8, tag_1:Utf8, tag_2:Utf8, tag_3:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
|
||||
@@ -5937,7 +5938,7 @@ async fn test_label_replace() {
|
||||
|
||||
let expected = r#"
|
||||
Filter: up.field_0 IS NOT NULL [timestamp:Timestamp(ms), field_0:Float64;N, foo:Utf8;N, tag_0:Utf8]
|
||||
Projection: up.timestamp, up.field_0, regexp_replace(up.tag_0, Utf8("^(?s:(.*):.*)$"), Utf8("$1")) AS foo, up.tag_0 [timestamp:Timestamp(ms), field_0:Float64;N, foo:Utf8;N, tag_0:Utf8]
|
||||
Projection: up.timestamp, up.field_0, CASE WHEN regexp_like(coalesce(up.tag_0, Utf8("")), Utf8("^(?s:(.*):.*)$")) THEN nullif(regexp_replace(coalesce(up.tag_0, Utf8("")), Utf8("^(?s:(.*):.*)$"), Utf8("$1")), Utf8("")) ELSE Utf8(NULL) END AS foo, up.tag_0 [timestamp:Timestamp(ms), field_0:Float64;N, foo:Utf8;N, tag_0:Utf8]
|
||||
PromInstantManipulate: range=[0..100000000], lookback=[1000], interval=[5000], time index=[timestamp] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
|
||||
PromSeriesDivide: tags=["tag_0"] [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
|
||||
Sort: up.tag_0 ASC NULLS FIRST, up.timestamp ASC NULLS FIRST [tag_0:Utf8, timestamp:Timestamp(ms), field_0:Float64;N]
|
||||
|
||||
@@ -1102,12 +1102,12 @@ async fn delta_offsets_survive_optimized_plan_serialization() {
|
||||
(
|
||||
"timestamp positive offset",
|
||||
r#"timestamp(delta_metric{series="cumulative"} offset 60s)"#,
|
||||
120.0,
|
||||
60.0,
|
||||
),
|
||||
(
|
||||
"timestamp negative offset",
|
||||
r#"timestamp(delta_metric{series="cumulative"} offset -60s)"#,
|
||||
120.0,
|
||||
180.0,
|
||||
),
|
||||
(
|
||||
"range positive offset",
|
||||
|
||||
@@ -1022,6 +1022,31 @@ pub async fn test_prometheus_label_replace_response(store_type: StorageType) {
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// A series whose source value does not match keeps its labels, without `host_copy`.
|
||||
let query = encode(r#"label_replace(demo, "host_copy", "$1", "host", "other(.*)")"#);
|
||||
let res = client
|
||||
.get(&format!("/v1/prometheus/api/v1/query?query={query}&time=0"))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body = res.json::<PrometheusJsonResponse>().await;
|
||||
assert_eq!(body.status, "success");
|
||||
assert_eq!(
|
||||
body.data,
|
||||
serde_json::from_value::<PrometheusResponse>(json!({
|
||||
"resultType": "vector",
|
||||
"result": [{
|
||||
"metric": {
|
||||
"__name__": "demo",
|
||||
"host": "host1"
|
||||
},
|
||||
"value": [0.0, "1.1"]
|
||||
}]
|
||||
}))
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
guard.remove_all().await;
|
||||
}
|
||||
|
||||
|
||||
@@ -621,9 +621,6 @@ TQL EVAL (330, 390, '60s') predict_linear(at_modifier_gauge{host="a"}[5m] offset
|
||||
-- on its own and `timestamp()` reports the timestamp of the sample the anchor selected at every
|
||||
-- step (300s here). Its plan keeps the anchored selection below the replay of the grid and projects
|
||||
-- the timestamp above it.
|
||||
--
|
||||
-- Only an `@` without an `offset` is asserted here: the value of `timestamp()` for an anchored
|
||||
-- selector that also carries an `offset` is a pre-existing question of its own, out of scope here.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 240, '60s') timestamp(at_modifier_gauge @ 300);
|
||||
|
||||
@@ -642,95 +639,105 @@ TQL EVAL (0, 240, '60s') timestamp(at_modifier_gauge @ 300);
|
||||
| 1970-01-01T00:04:00 | 300.0 | b |
|
||||
+---------------------+-------+------+
|
||||
|
||||
-- With an `offset`, the anchor selects the sample at 240s, and `timestamp()` reports 240s.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 120, '60s') timestamp(at_modifier_gauge @ 300 offset 1m);
|
||||
|
||||
+---------------------+-------+------+
|
||||
| ts | value | host |
|
||||
+---------------------+-------+------+
|
||||
| 1970-01-01T00:00:00 | 240.0 | a |
|
||||
| 1970-01-01T00:00:00 | 240.0 | b |
|
||||
| 1970-01-01T00:01:00 | 240.0 | a |
|
||||
| 1970-01-01T00:01:00 | 240.0 | b |
|
||||
| 1970-01-01T00:02:00 | 240.0 | a |
|
||||
| 1970-01-01T00:02:00 | 240.0 | b |
|
||||
+---------------------+-------+------+
|
||||
|
||||
-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED
|
||||
-- SQLNESS REPLACE (peers.*) REDACTED
|
||||
-- SQLNESS REPLACE (Hash.*) REDACTED
|
||||
-- SQLNESS REPLACE (RepartitionExec:.*) RepartitionExec: REDACTED
|
||||
TQL EXPLAIN (0, 240, '60s') timestamp(at_modifier_gauge @ 300);
|
||||
|
||||
+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| plan_type | plan |
|
||||
+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| logical_plan | MergeScan [is_placeholder=false, remote_input=[ |
|
||||
| | Projection: at_modifier_gauge.ts, value AS value, at_modifier_gauge.host |
|
||||
| | Projection: at_modifier_gauge.ts, __promql_timestamp_value_ AS value, at_modifier_gauge.host |
|
||||
| | PromInstantManipulate: range=[0..240000], lookback=[240001], interval=[60000], time index=[ts] |
|
||||
| | PromInstantManipulate: range=[0..0], lookback=[300000], interval=[60000], time index=[ts] |
|
||||
| | Projection: at_modifier_gauge.ts, at_modifier_gauge.val, at_modifier_gauge.host, CAST(CAST(CAST(CAST(CAST(at_modifier_gauge.ts AS Int64) AS Decimal128(19, 0)) AS Decimal128(21, 0)) + Decimal128(0,19,0) AS Int64) AS Float64) / Float64(1000) AS __promql_timestamp_value_ |
|
||||
| | PromSeriesNormalize: offset=[-300000], time index=[ts], filter NaN: [false] |
|
||||
| | PromSeriesDivide: tags=["host"] |
|
||||
| | Sort: at_modifier_gauge.host ASC NULLS FIRST, at_modifier_gauge.ts ASC NULLS FIRST |
|
||||
| | Filter: at_modifier_gauge.ts >= TimestampMillisecond(1, None) AND at_modifier_gauge.ts <= TimestampMillisecond(300000, None) |
|
||||
| | TableScan: at_modifier_gauge, partial_filters=[at_modifier_gauge.ts >= TimestampMillisecond(1, None), at_modifier_gauge.ts <= TimestampMillisecond(300000, None)] |
|
||||
| | ]] |
|
||||
| physical_plan | CooperativeExec |
|
||||
+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| plan_type | plan |
|
||||
+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| logical_plan | MergeScan [is_placeholder=false, remote_input=[ |
|
||||
| | Projection: at_modifier_gauge.ts, value AS value, at_modifier_gauge.host |
|
||||
| | Projection: at_modifier_gauge.ts, __promql_timestamp_value_ AS value, at_modifier_gauge.host |
|
||||
| | Filter: at_modifier_gauge.val IS NOT NULL |
|
||||
| | PromInstantManipulate: range=[0..240000], lookback=[240001], interval=[60000], time index=[ts] |
|
||||
| | PromInstantManipulate: range=[0..0], lookback=[300000], interval=[60000], time index=[ts] |
|
||||
| | Projection: at_modifier_gauge.ts, at_modifier_gauge.val, at_modifier_gauge.host, CAST(CAST(at_modifier_gauge.ts AS Int64) AS Float64) / Float64(1000) AS __promql_timestamp_value_ |
|
||||
| | PromSeriesNormalize: offset=[-300000], time index=[ts], filter NaN: [false] |
|
||||
| | PromSeriesDivide: tags=["host"] |
|
||||
| | Sort: at_modifier_gauge.host ASC NULLS FIRST, at_modifier_gauge.ts ASC NULLS FIRST |
|
||||
| | Filter: at_modifier_gauge.ts >= TimestampMillisecond(1, None) AND at_modifier_gauge.ts <= TimestampMillisecond(300000, None) |
|
||||
| | TableScan: at_modifier_gauge, partial_filters=[at_modifier_gauge.ts >= TimestampMillisecond(1, None), at_modifier_gauge.ts <= TimestampMillisecond(300000, None)] |
|
||||
| | ]] |
|
||||
| physical_plan | CooperativeExec |
|
||||
| | MergeScanExec: REDACTED
|
||||
| | |
|
||||
+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| | |
|
||||
+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
-- 11. `label_join` above an anchored selector. The call rewrites the label the input series are
|
||||
-- told apart by (`host` becomes the empty string), so it is never promoted: it must stay above the
|
||||
-- told apart by (`host` becomes `a-a` and `b-b`), so it is never promoted: it must stay above the
|
||||
-- per-series replay of the selector and be evaluated at every step. Both hosts are still reported,
|
||||
-- with their own value, at every step. A promoted `label_join` would instead be replayed through
|
||||
-- the labels it just rewrote and report the two series as a single timeline, i.e. one mixed row
|
||||
-- per step.
|
||||
--
|
||||
-- The invariant asserted here is scoped to the replay: it must not drop or mix the input rows.
|
||||
-- It is not a claim about the final PromQL semantics of this query: joining `host` to one value
|
||||
-- leaves two samples with the same label set at the same timestamp, which Prometheus rejects,
|
||||
-- while `label_join` does not validate that yet (`label_replace` errors on such a rewrite
|
||||
-- instead). That duplicate-labelset validation gap is pre-existing and out of scope here.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (300, 480, '60s') label_join(at_modifier_gauge @ 300, "host", "", "");
|
||||
TQL EVAL (300, 480, '60s') label_join(at_modifier_gauge @ 300, "host", "-", "host", "host");
|
||||
|
||||
+---------------------+------+------+
|
||||
| ts | val | host |
|
||||
+---------------------+------+------+
|
||||
| 1970-01-01T00:05:00 | 12.0 | |
|
||||
| 1970-01-01T00:05:00 | 5.0 | |
|
||||
| 1970-01-01T00:06:00 | 12.0 | |
|
||||
| 1970-01-01T00:06:00 | 5.0 | |
|
||||
| 1970-01-01T00:07:00 | 12.0 | |
|
||||
| 1970-01-01T00:07:00 | 5.0 | |
|
||||
| 1970-01-01T00:08:00 | 12.0 | |
|
||||
| 1970-01-01T00:08:00 | 5.0 | |
|
||||
| 1970-01-01T00:05:00 | 12.0 | b-b |
|
||||
| 1970-01-01T00:05:00 | 5.0 | a-a |
|
||||
| 1970-01-01T00:06:00 | 12.0 | b-b |
|
||||
| 1970-01-01T00:06:00 | 5.0 | a-a |
|
||||
| 1970-01-01T00:07:00 | 12.0 | b-b |
|
||||
| 1970-01-01T00:07:00 | 5.0 | a-a |
|
||||
| 1970-01-01T00:08:00 | 12.0 | b-b |
|
||||
| 1970-01-01T00:08:00 | 5.0 | a-a |
|
||||
+---------------------+------+------+
|
||||
|
||||
-- The same join below another call: neither is promoted, and both are evaluated at every step over
|
||||
-- the per-series replay of the anchored selector.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (300, 480, '60s') abs(label_join(at_modifier_gauge @ 300, "host", "", ""));
|
||||
TQL EVAL (300, 480, '60s') abs(label_join(at_modifier_gauge @ 300, "host", "-", "host", "host"));
|
||||
|
||||
+---------------------+----------+------+
|
||||
| ts | abs(val) | host |
|
||||
+---------------------+----------+------+
|
||||
| 1970-01-01T00:05:00 | 12.0 | |
|
||||
| 1970-01-01T00:05:00 | 5.0 | |
|
||||
| 1970-01-01T00:06:00 | 12.0 | |
|
||||
| 1970-01-01T00:06:00 | 5.0 | |
|
||||
| 1970-01-01T00:07:00 | 12.0 | |
|
||||
| 1970-01-01T00:07:00 | 5.0 | |
|
||||
| 1970-01-01T00:08:00 | 12.0 | |
|
||||
| 1970-01-01T00:08:00 | 5.0 | |
|
||||
| 1970-01-01T00:05:00 | 12.0 | b-b |
|
||||
| 1970-01-01T00:05:00 | 5.0 | a-a |
|
||||
| 1970-01-01T00:06:00 | 12.0 | b-b |
|
||||
| 1970-01-01T00:06:00 | 5.0 | a-a |
|
||||
| 1970-01-01T00:07:00 | 12.0 | b-b |
|
||||
| 1970-01-01T00:07:00 | 5.0 | a-a |
|
||||
| 1970-01-01T00:08:00 | 12.0 | b-b |
|
||||
| 1970-01-01T00:08:00 | 5.0 | a-a |
|
||||
+---------------------+----------+------+
|
||||
|
||||
-- A range call below the join is still promoted on its own (it is the direct call over the
|
||||
-- anchored range selector): the anchored window is folded once per series, and the join above that
|
||||
-- replay reports the rate of both hosts at every step.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (300, 480, '60s') label_join(rate(at_modifier_counter_total[5m] @ 300), "host", "", "");
|
||||
TQL EVAL (300, 480, '60s') label_join(rate(at_modifier_counter_total[5m] @ 300), "host", "-", "host", "host");
|
||||
|
||||
+---------------------+------------------------------------------+------+
|
||||
| ts | prom_rate(ts_range,val,ts,Int64(300000)) | host |
|
||||
+---------------------+------------------------------------------+------+
|
||||
| 1970-01-01T00:05:00 | 0.5 | |
|
||||
| 1970-01-01T00:05:00 | 1.0 | |
|
||||
| 1970-01-01T00:06:00 | 0.5 | |
|
||||
| 1970-01-01T00:06:00 | 1.0 | |
|
||||
| 1970-01-01T00:07:00 | 0.5 | |
|
||||
| 1970-01-01T00:07:00 | 1.0 | |
|
||||
| 1970-01-01T00:08:00 | 0.5 | |
|
||||
| 1970-01-01T00:08:00 | 1.0 | |
|
||||
| 1970-01-01T00:05:00 | 0.5 | b-b |
|
||||
| 1970-01-01T00:05:00 | 1.0 | a-a |
|
||||
| 1970-01-01T00:06:00 | 0.5 | b-b |
|
||||
| 1970-01-01T00:06:00 | 1.0 | a-a |
|
||||
| 1970-01-01T00:07:00 | 0.5 | b-b |
|
||||
| 1970-01-01T00:07:00 | 1.0 | a-a |
|
||||
| 1970-01-01T00:08:00 | 0.5 | b-b |
|
||||
| 1970-01-01T00:08:00 | 1.0 | a-a |
|
||||
+---------------------+------------------------------------------+------+
|
||||
|
||||
DROP TABLE at_modifier_gauge;
|
||||
|
||||
@@ -258,12 +258,13 @@ TQL EVAL (330, 390, '60s') predict_linear(at_modifier_gauge{host="a"}[5m] offset
|
||||
-- on its own and `timestamp()` reports the timestamp of the sample the anchor selected at every
|
||||
-- step (300s here). Its plan keeps the anchored selection below the replay of the grid and projects
|
||||
-- the timestamp above it.
|
||||
--
|
||||
-- Only an `@` without an `offset` is asserted here: the value of `timestamp()` for an anchored
|
||||
-- selector that also carries an `offset` is a pre-existing question of its own, out of scope here.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 240, '60s') timestamp(at_modifier_gauge @ 300);
|
||||
|
||||
-- With an `offset`, the anchor selects the sample at 240s, and `timestamp()` reports 240s.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 120, '60s') timestamp(at_modifier_gauge @ 300 offset 1m);
|
||||
|
||||
-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED
|
||||
-- SQLNESS REPLACE (peers.*) REDACTED
|
||||
-- SQLNESS REPLACE (Hash.*) REDACTED
|
||||
@@ -271,30 +272,24 @@ TQL EVAL (0, 240, '60s') timestamp(at_modifier_gauge @ 300);
|
||||
TQL EXPLAIN (0, 240, '60s') timestamp(at_modifier_gauge @ 300);
|
||||
|
||||
-- 11. `label_join` above an anchored selector. The call rewrites the label the input series are
|
||||
-- told apart by (`host` becomes the empty string), so it is never promoted: it must stay above the
|
||||
-- told apart by (`host` becomes `a-a` and `b-b`), so it is never promoted: it must stay above the
|
||||
-- per-series replay of the selector and be evaluated at every step. Both hosts are still reported,
|
||||
-- with their own value, at every step. A promoted `label_join` would instead be replayed through
|
||||
-- the labels it just rewrote and report the two series as a single timeline, i.e. one mixed row
|
||||
-- per step.
|
||||
--
|
||||
-- The invariant asserted here is scoped to the replay: it must not drop or mix the input rows.
|
||||
-- It is not a claim about the final PromQL semantics of this query: joining `host` to one value
|
||||
-- leaves two samples with the same label set at the same timestamp, which Prometheus rejects,
|
||||
-- while `label_join` does not validate that yet (`label_replace` errors on such a rewrite
|
||||
-- instead). That duplicate-labelset validation gap is pre-existing and out of scope here.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (300, 480, '60s') label_join(at_modifier_gauge @ 300, "host", "", "");
|
||||
TQL EVAL (300, 480, '60s') label_join(at_modifier_gauge @ 300, "host", "-", "host", "host");
|
||||
|
||||
-- The same join below another call: neither is promoted, and both are evaluated at every step over
|
||||
-- the per-series replay of the anchored selector.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (300, 480, '60s') abs(label_join(at_modifier_gauge @ 300, "host", "", ""));
|
||||
TQL EVAL (300, 480, '60s') abs(label_join(at_modifier_gauge @ 300, "host", "-", "host", "host"));
|
||||
|
||||
-- A range call below the join is still promoted on its own (it is the direct call over the
|
||||
-- anchored range selector): the anchored window is folded once per series, and the join above that
|
||||
-- replay reports the rate of both hosts at every step.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (300, 480, '60s') label_join(rate(at_modifier_counter_total[5m] @ 300), "host", "", "");
|
||||
TQL EVAL (300, 480, '60s') label_join(rate(at_modifier_counter_total[5m] @ 300), "host", "-", "host", "host");
|
||||
|
||||
DROP TABLE at_modifier_gauge;
|
||||
|
||||
|
||||
@@ -21,9 +21,23 @@ INSERT INTO TABLE test VALUES
|
||||
Affected Rows: 8
|
||||
|
||||
-- Missing source labels --
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_join(test{host="host1"}, "new_host", "-");
|
||||
|
||||
Error: 1004(InvalidArguments), Invalid function argument for label_join
|
||||
+---------------------+-----+----------+-------+------------+
|
||||
| ts | val | new_host | host | idc |
|
||||
+---------------------+-----+----------+-------+------------+
|
||||
| 1970-01-01T00:00:00 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 3 | | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:10 | 3 | | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 5 | | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:15 | 3 | | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:15 | 5 | | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 7 | | host1 | idc4:zone3 |
|
||||
+---------------------+-----+----------+-------+------------+
|
||||
|
||||
-- dst_label is equal to source label --
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
@@ -63,24 +77,27 @@ TQL EVAL (0, 15, '5s') label_join(test{host="host1"}, "host", "-", "idc", "host"
|
||||
| 1970-01-01T00:00:15 | 7 | idc4:zone3-host1 | idc4:zone3 |
|
||||
+---------------------+-----+------------------+------------+
|
||||
|
||||
-- test the empty source label --
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
-- Absent labels join as empty strings and keep their separators --
|
||||
TQL EVAL (0, 15, '5s') label_join(label_join(vector(1), "a", "", "missing"), "b", "-", "a", "a");
|
||||
|
||||
+---------------------+----------------+---+---+
|
||||
| time | greptime_value | b | a |
|
||||
+---------------------+----------------+---+---+
|
||||
| 1970-01-01T00:00:00 | 1.0 | - | |
|
||||
| 1970-01-01T00:00:05 | 1.0 | - | |
|
||||
| 1970-01-01T00:00:10 | 1.0 | - | |
|
||||
| 1970-01-01T00:00:15 | 1.0 | - | |
|
||||
+---------------------+----------------+---+---+
|
||||
|
||||
-- An empty source label name is invalid --
|
||||
TQL EVAL (0, 15, '5s') label_join(test{host="host1"}, "host", "-", "");
|
||||
|
||||
+---------------------+-----+------+------------+
|
||||
| ts | val | host | idc |
|
||||
+---------------------+-----+------+------------+
|
||||
| 1970-01-01T00:00:00 | 1 | | idc1 |
|
||||
| 1970-01-01T00:00:05 | 1 | | idc1 |
|
||||
| 1970-01-01T00:00:05 | 3 | | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 1 | | idc1 |
|
||||
| 1970-01-01T00:00:10 | 3 | | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 5 | | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 1 | | idc1 |
|
||||
| 1970-01-01T00:00:15 | 3 | | idc2:zone1 |
|
||||
| 1970-01-01T00:00:15 | 5 | | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 7 | | idc4:zone3 |
|
||||
+---------------------+-----+------+------------+
|
||||
Error: 1004(InvalidArguments), Invalid function argument for label_join
|
||||
|
||||
-- Both hosts are joined to the same `host` at 0s, leaving two series with the same label set --
|
||||
TQL EVAL (0, 15, '5s') label_join(test, "host", "-", "idc");
|
||||
|
||||
Error: 3001(EngineExecuteQuery), Execution error: vector cannot contain metrics with the same labelset
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_join(test{host="host1"}, "new_host", "-", "idc", "host");
|
||||
@@ -114,13 +131,13 @@ TQL EVAL (0, 15, '5s') label_replace(test{host="host1"}, "new_idc", "$2", "idc",
|
||||
+---------------------+-----+---------+-------+------------+
|
||||
| ts | val | new_idc | host | idc |
|
||||
+---------------------+-----+---------+-------+------------+
|
||||
| 1970-01-01T00:00:00 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:00 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 3 | zone1 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:10 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:10 | 3 | zone1 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 5 | zone2 | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:15 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:15 | 3 | zone1 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:15 | 5 | zone2 | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 7 | zone3 | host1 | idc4:zone3 |
|
||||
@@ -129,20 +146,20 @@ TQL EVAL (0, 15, '5s') label_replace(test{host="host1"}, "new_idc", "$2", "idc",
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_replace(test{host="host1"}, "new_idc", "idc99", "idc", "idc2.*");
|
||||
|
||||
+---------------------+-----+------------+-------+------------+
|
||||
| ts | val | new_idc | host | idc |
|
||||
+---------------------+-----+------------+-------+------------+
|
||||
| 1970-01-01T00:00:00 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 3 | idc99 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:10 | 3 | idc99 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 5 | idc3:zone2 | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:15 | 3 | idc99 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:15 | 5 | idc3:zone2 | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 7 | idc4:zone3 | host1 | idc4:zone3 |
|
||||
+---------------------+-----+------------+-------+------------+
|
||||
+---------------------+-----+---------+-------+------------+
|
||||
| ts | val | new_idc | host | idc |
|
||||
+---------------------+-----+---------+-------+------------+
|
||||
| 1970-01-01T00:00:00 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 3 | idc99 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:10 | 3 | idc99 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 5 | | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:15 | 3 | idc99 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:15 | 5 | | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 7 | | host1 | idc4:zone3 |
|
||||
+---------------------+-----+---------+-------+------------+
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_replace(test{host="host2"}, "new_idc", "$2", "idc", "(.*):(.*)");
|
||||
@@ -150,26 +167,61 @@ TQL EVAL (0, 15, '5s') label_replace(test{host="host2"}, "new_idc", "$2", "idc",
|
||||
+---------------------+-----+---------+-------+------+
|
||||
| ts | val | new_idc | host | idc |
|
||||
+---------------------+-----+---------+-------+------+
|
||||
| 1970-01-01T00:00:00 | 2 | idc1 | host2 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 2 | idc1 | host2 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 4 | idc2 | host2 | idc2 |
|
||||
| 1970-01-01T00:00:10 | 2 | idc1 | host2 | idc1 |
|
||||
| 1970-01-01T00:00:10 | 4 | idc2 | host2 | idc2 |
|
||||
| 1970-01-01T00:00:10 | 6 | idc3 | host2 | idc3 |
|
||||
| 1970-01-01T00:00:15 | 2 | idc1 | host2 | idc1 |
|
||||
| 1970-01-01T00:00:15 | 4 | idc2 | host2 | idc2 |
|
||||
| 1970-01-01T00:00:15 | 6 | idc3 | host2 | idc3 |
|
||||
| 1970-01-01T00:00:15 | 8 | idc4 | host2 | idc4 |
|
||||
| 1970-01-01T00:00:00 | 2 | | host2 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 2 | | host2 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 4 | | host2 | idc2 |
|
||||
| 1970-01-01T00:00:10 | 2 | | host2 | idc1 |
|
||||
| 1970-01-01T00:00:10 | 4 | | host2 | idc2 |
|
||||
| 1970-01-01T00:00:10 | 6 | | host2 | idc3 |
|
||||
| 1970-01-01T00:00:15 | 2 | | host2 | idc1 |
|
||||
| 1970-01-01T00:00:15 | 4 | | host2 | idc2 |
|
||||
| 1970-01-01T00:00:15 | 6 | | host2 | idc3 |
|
||||
| 1970-01-01T00:00:15 | 8 | | host2 | idc4 |
|
||||
+---------------------+-----+---------+-------+------+
|
||||
|
||||
-- dst_label is equal to source label --
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_replace(test{host="host2"}, "idc", "$2", "idc", "(.*):(.*)");
|
||||
|
||||
Error: 1004(InvalidArguments), vector cannot contain metrics with the same labelset
|
||||
+---------------------+-----+------+-------+
|
||||
| ts | val | idc | host |
|
||||
+---------------------+-----+------+-------+
|
||||
| 1970-01-01T00:00:00 | 2 | idc1 | host2 |
|
||||
| 1970-01-01T00:00:05 | 2 | idc1 | host2 |
|
||||
| 1970-01-01T00:00:05 | 4 | idc2 | host2 |
|
||||
| 1970-01-01T00:00:10 | 2 | idc1 | host2 |
|
||||
| 1970-01-01T00:00:10 | 4 | idc2 | host2 |
|
||||
| 1970-01-01T00:00:10 | 6 | idc3 | host2 |
|
||||
| 1970-01-01T00:00:15 | 2 | idc1 | host2 |
|
||||
| 1970-01-01T00:00:15 | 4 | idc2 | host2 |
|
||||
| 1970-01-01T00:00:15 | 6 | idc3 | host2 |
|
||||
| 1970-01-01T00:00:15 | 8 | idc4 | host2 |
|
||||
+---------------------+-----+------+-------+
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_replace(test{host="host1"}, "idc", "$2", "idc", "(.*):(.*)");
|
||||
|
||||
+---------------------+-----+-------+-------+
|
||||
| ts | val | idc | host |
|
||||
+---------------------+-----+-------+-------+
|
||||
| 1970-01-01T00:00:00 | 1 | idc1 | host1 |
|
||||
| 1970-01-01T00:00:05 | 1 | idc1 | host1 |
|
||||
| 1970-01-01T00:00:05 | 3 | zone1 | host1 |
|
||||
| 1970-01-01T00:00:10 | 1 | idc1 | host1 |
|
||||
| 1970-01-01T00:00:10 | 3 | zone1 | host1 |
|
||||
| 1970-01-01T00:00:10 | 5 | zone2 | host1 |
|
||||
| 1970-01-01T00:00:15 | 1 | idc1 | host1 |
|
||||
| 1970-01-01T00:00:15 | 3 | zone1 | host1 |
|
||||
| 1970-01-01T00:00:15 | 5 | zone2 | host1 |
|
||||
| 1970-01-01T00:00:15 | 7 | zone3 | host1 |
|
||||
+---------------------+-----+-------+-------+
|
||||
|
||||
-- Both hosts are rewritten to the same `host`, leaving two series with the same label set --
|
||||
TQL EVAL (0, 15, '5s') label_replace(test, "host", "x", "host", ".*");
|
||||
|
||||
Error: 3001(EngineExecuteQuery), Execution error: vector cannot contain metrics with the same labelset
|
||||
|
||||
-- test the empty source label --
|
||||
-- TODO(dennis): we can't remove the label currently --
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_replace(test{host="host2"}, "idc2", "", "", "");
|
||||
|
||||
@@ -221,20 +273,20 @@ TQL EVAL(0, 15, '5s') {__name__="test",host="host1"} + label_replace(vector(1),
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL(0, 15, '5s') label_replace(test{host="host1"}, "host2", "host2", "host", "");
|
||||
|
||||
+---------------------+-----+-------+------------+
|
||||
| ts | val | host | idc |
|
||||
+---------------------+-----+-------+------------+
|
||||
| 1970-01-01T00:00:00 | 1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 3 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:10 | 3 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 5 | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:15 | 3 | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:15 | 5 | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 7 | host1 | idc4:zone3 |
|
||||
+---------------------+-----+-------+------------+
|
||||
+---------------------+-----+-------+-------+------------+
|
||||
| ts | val | host2 | host | idc |
|
||||
+---------------------+-----+-------+-------+------------+
|
||||
| 1970-01-01T00:00:00 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 3 | | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:10 | 3 | | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:10 | 5 | | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:15 | 3 | | host1 | idc2:zone1 |
|
||||
| 1970-01-01T00:00:15 | 5 | | host1 | idc3:zone2 |
|
||||
| 1970-01-01T00:00:15 | 7 | | host1 | idc4:zone3 |
|
||||
+---------------------+-----+-------+-------+------------+
|
||||
|
||||
-- Empty regex with not existing source label
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
@@ -274,6 +326,23 @@ TQL EVAL(0, 15, '5s') label_replace(test{host="host1"}, "host2", "", "instance",
|
||||
| 1970-01-01T00:00:15 | 7 | host1 | idc4:zone3 |
|
||||
+---------------------+-----+-------+------------+
|
||||
|
||||
-- An empty replacement removes an existing label --
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_replace(test{idc="idc1"}, "idc", "", "instance", "");
|
||||
|
||||
+---------------------+-----+-----+-------+
|
||||
| ts | val | idc | host |
|
||||
+---------------------+-----+-----+-------+
|
||||
| 1970-01-01T00:00:00 | 1 | | host1 |
|
||||
| 1970-01-01T00:00:00 | 2 | | host2 |
|
||||
| 1970-01-01T00:00:05 | 1 | | host1 |
|
||||
| 1970-01-01T00:00:05 | 2 | | host2 |
|
||||
| 1970-01-01T00:00:10 | 1 | | host1 |
|
||||
| 1970-01-01T00:00:10 | 2 | | host2 |
|
||||
| 1970-01-01T00:00:15 | 1 | | host1 |
|
||||
| 1970-01-01T00:00:15 | 2 | | host2 |
|
||||
+---------------------+-----+-----+-------+
|
||||
|
||||
-- Empty regex and different label value
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL(0, 15, '5s') {__name__="test",host="host1"} * label_replace(vector(1), "host", "host2", "host", "");
|
||||
@@ -307,10 +376,10 @@ TQL EVAL (0, 15, '5s') label_replace(test{host="host1"}, "new_idc", "idc99", "id
|
||||
+---------------------+-----+---------+-------+------+
|
||||
| ts | val | new_idc | host | idc |
|
||||
+---------------------+-----+---------+-------+------+
|
||||
| 1970-01-01T00:00:00 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:10 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:15 | 1 | idc1 | host1 | idc1 |
|
||||
| 1970-01-01T00:00:00 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:05 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:10 | 1 | | host1 | idc1 |
|
||||
| 1970-01-01T00:00:15 | 1 | | host1 | idc1 |
|
||||
+---------------------+-----+---------+-------+------+
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
|
||||
@@ -17,6 +17,7 @@ INSERT INTO TABLE test VALUES
|
||||
(15000, 'host2', 'idc4',8);
|
||||
|
||||
-- Missing source labels --
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_join(test{host="host1"}, "new_host", "-");
|
||||
|
||||
-- dst_label is equal to source label --
|
||||
@@ -27,10 +28,15 @@ TQL EVAL (0, 15, '5s') label_join(test{host="host1"}, "host", "-", "host");
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_join(test{host="host1"}, "host", "-", "idc", "host");
|
||||
|
||||
-- test the empty source label --
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
-- Absent labels join as empty strings and keep their separators --
|
||||
TQL EVAL (0, 15, '5s') label_join(label_join(vector(1), "a", "", "missing"), "b", "-", "a", "a");
|
||||
|
||||
-- An empty source label name is invalid --
|
||||
TQL EVAL (0, 15, '5s') label_join(test{host="host1"}, "host", "-", "");
|
||||
|
||||
-- Both hosts are joined to the same `host` at 0s, leaving two series with the same label set --
|
||||
TQL EVAL (0, 15, '5s') label_join(test, "host", "-", "idc");
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_join(test{host="host1"}, "new_host", "-", "idc", "host");
|
||||
|
||||
@@ -50,8 +56,13 @@ TQL EVAL (0, 15, '5s') label_replace(test{host="host2"}, "new_idc", "$2", "idc",
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_replace(test{host="host2"}, "idc", "$2", "idc", "(.*):(.*)");
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_replace(test{host="host1"}, "idc", "$2", "idc", "(.*):(.*)");
|
||||
|
||||
-- Both hosts are rewritten to the same `host`, leaving two series with the same label set --
|
||||
TQL EVAL (0, 15, '5s') label_replace(test, "host", "x", "host", ".*");
|
||||
|
||||
-- test the empty source label --
|
||||
-- TODO(dennis): we can't remove the label currently --
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_replace(test{host="host2"}, "idc2", "", "", "");
|
||||
|
||||
@@ -77,6 +88,10 @@ TQL EVAL(0, 15, '5s') label_replace(test{host="host1"}, "host2", "host2", "insta
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL(0, 15, '5s') label_replace(test{host="host1"}, "host2", "", "instance", "");
|
||||
|
||||
-- An empty replacement removes an existing label --
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (0, 15, '5s') label_replace(test{idc="idc1"}, "idc", "", "instance", "");
|
||||
|
||||
-- Empty regex and different label value
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL(0, 15, '5s') {__name__="test",host="host1"} * label_replace(vector(1), "host", "host2", "host", "");
|
||||
|
||||
@@ -613,6 +613,15 @@ TQL EVAL (1, 1, '1s', '1s') native_time_sec offset -500ms;
|
||||
| 11.0 | 1970-01-01T00:00:01 |
|
||||
+------+---------------------+
|
||||
|
||||
-- `timestamp()` reports the timestamp of the selected sample (1s), without the offset applied.
|
||||
TQL EVAL (1, 1, '1s', '1s') timestamp(native_time_sec offset -500ms);
|
||||
|
||||
+---------------------+-------+
|
||||
| ts | value |
|
||||
+---------------------+-------+
|
||||
| 1970-01-01T00:00:01 | 1.0 |
|
||||
+---------------------+-------+
|
||||
|
||||
DROP TABLE native_time_sec;
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
@@ -219,4 +219,6 @@ CREATE TABLE native_time_sec (ts TIMESTAMP(0) TIME INDEX, val DOUBLE);
|
||||
INSERT INTO native_time_sec VALUES (0, 10), (1, 11), (2, 12);
|
||||
TQL EVAL (1, 1, '1s', '1s') native_time_sec offset 500ms;
|
||||
TQL EVAL (1, 1, '1s', '1s') native_time_sec offset -500ms;
|
||||
-- `timestamp()` reports the timestamp of the selected sample (1s), without the offset applied.
|
||||
TQL EVAL (1, 1, '1s', '1s') timestamp(native_time_sec offset -500ms);
|
||||
DROP TABLE native_time_sec;
|
||||
|
||||
@@ -113,17 +113,17 @@ tql eval (0, 60, '30s') abs(timestamp(timestamp_test) - avg(timestamp(timestamp_
|
||||
-- Test Issue 6707
|
||||
tql eval timestamp(demo_memory_usage_bytes * 1);
|
||||
|
||||
+------+--------------------+
|
||||
| time | value * Float64(1) |
|
||||
+------+--------------------+
|
||||
+------+--------------------+
|
||||
+------+-------+
|
||||
| time | value |
|
||||
+------+-------+
|
||||
+------+-------+
|
||||
|
||||
tql eval timestamp(-demo_memory_usage_bytes);
|
||||
|
||||
+------+-----------+
|
||||
| time | (- value) |
|
||||
+------+-----------+
|
||||
+------+-----------+
|
||||
+------+-------+
|
||||
| time | value |
|
||||
+------+-------+
|
||||
+------+-------+
|
||||
|
||||
tql eval (0, 60, '30s') timestamp(timestamp_test) == 60;
|
||||
|
||||
@@ -133,6 +133,79 @@ tql eval (0, 60, '30s') timestamp(timestamp_test) == 60;
|
||||
| 1970-01-01T00:01:00 | 60.0 |
|
||||
+---------------------+-------+
|
||||
|
||||
-- timestamp() reports the timestamp of the selected sample, not the evaluation time
|
||||
tql eval (60, 60, '1s') timestamp(timestamp_test offset 30s);
|
||||
|
||||
+---------------------+-------+
|
||||
| ts | value |
|
||||
+---------------------+-------+
|
||||
| 1970-01-01T00:01:00 | 1.0 |
|
||||
+---------------------+-------+
|
||||
|
||||
tql eval (60, 60, '1s') timestamp((timestamp_test offset 30s));
|
||||
|
||||
+---------------------+-------+
|
||||
| ts | value |
|
||||
+---------------------+-------+
|
||||
| 1970-01-01T00:01:00 | 1.0 |
|
||||
+---------------------+-------+
|
||||
|
||||
-- The result of a function is sampled at the evaluation time
|
||||
tql eval (90, 90, '1s') timestamp(abs(timestamp_test));
|
||||
|
||||
+---------------------+-------+
|
||||
| ts | value |
|
||||
+---------------------+-------+
|
||||
| 1970-01-01T00:01:30 | 90.0 |
|
||||
+---------------------+-------+
|
||||
|
||||
-- A row whose fields are all NULL holds no sample and gets no timestamp; a row with any
|
||||
-- non-NULL field does.
|
||||
create table timestamp_null (ts timestamp time index, host string primary key, a double, b double);
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
insert into timestamp_null values
|
||||
(0, 'none', NULL, NULL),
|
||||
(0, 'only_a', 1.0, NULL),
|
||||
(0, 'only_b', NULL, 2.0);
|
||||
|
||||
Affected Rows: 3
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
tql eval (60, 60, '1s') timestamp(timestamp_null);
|
||||
|
||||
+---------------------+-------+--------+
|
||||
| ts | value | host |
|
||||
+---------------------+-------+--------+
|
||||
| 1970-01-01T00:01:00 | 0.0 | only_a |
|
||||
| 1970-01-01T00:01:00 | 0.0 | only_b |
|
||||
+---------------------+-------+--------+
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
tql eval (60, 60, '1s') timestamp(-timestamp_null);
|
||||
|
||||
+---------------------+-------+--------+
|
||||
| ts | value | host |
|
||||
+---------------------+-------+--------+
|
||||
| 1970-01-01T00:01:00 | 60.0 | only_a |
|
||||
| 1970-01-01T00:01:00 | 60.0 | only_b |
|
||||
+---------------------+-------+--------+
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
tql eval (60, 60, '1s') timestamp(abs(timestamp_null));
|
||||
|
||||
+---------------------+-------+--------+
|
||||
| ts | value | host |
|
||||
+---------------------+-------+--------+
|
||||
| 1970-01-01T00:01:00 | 60.0 | only_a |
|
||||
| 1970-01-01T00:01:00 | 60.0 | only_b |
|
||||
+---------------------+-------+--------+
|
||||
|
||||
drop table timestamp_null;
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
-- Test timestamp() with multiple metrics
|
||||
create table timestamp_test2 (ts timestamp time index, val double);
|
||||
|
||||
|
||||
@@ -43,6 +43,34 @@ tql eval timestamp(-demo_memory_usage_bytes);
|
||||
|
||||
tql eval (0, 60, '30s') timestamp(timestamp_test) == 60;
|
||||
|
||||
-- timestamp() reports the timestamp of the selected sample, not the evaluation time
|
||||
tql eval (60, 60, '1s') timestamp(timestamp_test offset 30s);
|
||||
|
||||
tql eval (60, 60, '1s') timestamp((timestamp_test offset 30s));
|
||||
|
||||
-- The result of a function is sampled at the evaluation time
|
||||
tql eval (90, 90, '1s') timestamp(abs(timestamp_test));
|
||||
|
||||
-- A row whose fields are all NULL holds no sample and gets no timestamp; a row with any
|
||||
-- non-NULL field does.
|
||||
create table timestamp_null (ts timestamp time index, host string primary key, a double, b double);
|
||||
|
||||
insert into timestamp_null values
|
||||
(0, 'none', NULL, NULL),
|
||||
(0, 'only_a', 1.0, NULL),
|
||||
(0, 'only_b', NULL, 2.0);
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
tql eval (60, 60, '1s') timestamp(timestamp_null);
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
tql eval (60, 60, '1s') timestamp(-timestamp_null);
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
tql eval (60, 60, '1s') timestamp(abs(timestamp_null));
|
||||
|
||||
drop table timestamp_null;
|
||||
|
||||
-- Test timestamp() with multiple metrics
|
||||
create table timestamp_test2 (ts timestamp time index, val double);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user