Files
greptimedb/tests/cases/standalone/common/promql/at_modifier.sql
T
dennis zhuang cedd078440 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>
2026-10-12 12:49:20 +00:00

297 lines
15 KiB
SQL

-- Tests for the PromQL `@` modifier on vector and matrix selectors.
--
-- `@` anchors the sample selection window at a fixed timestamp instead of the timestamp of each
-- evaluation step: `@ <unix_ts>` uses the given timestamp, `@ start()` / `@ end()` use the
-- start/end of the statement's evaluation range, and `offset` shifts the anchor backwards before
-- the window is selected. The output timestamps still follow the evaluation grid.
--
-- Every metric below carries two series, host 'a' and host 'b', with different values: the
-- anchored results must therefore report both of them, per step. A series silently dropped, or a
-- batch holding several series handled as one timeline, does not look like a correct single-series
-- answer here but shows up as a missing or shifted row.
--
-- Sample timestamps below are in milliseconds, while `@` and `TQL EVAL` timestamps are in seconds,
-- as in Prometheus.
CREATE TABLE at_modifier_gauge (
ts TIMESTAMP TIME INDEX,
val DOUBLE,
host STRING,
PRIMARY KEY(host)
);
CREATE TABLE at_modifier_counter_total (
ts TIMESTAMP TIME INDEX,
val DOUBLE,
host STRING,
PRIMARY KEY(host)
);
-- One sample per minute, so the anchored sample is easy to identify. The default lookback is 5m.
-- Host 'a' reports 0.0 to 6.0, host 'b' reports 7.0 to 13.0: the two series are told apart by their
-- values alone, which stays true after any anchoring.
INSERT INTO at_modifier_gauge VALUES
(0, 0.0, 'a'),
(60000, 1.0, 'a'),
(120000, 2.0, 'a'),
(180000, 3.0, 'a'),
(240000, 4.0, 'a'),
(300000, 5.0, 'a'),
(360000, 6.0, 'a'),
(0, 7.0, 'b'),
(60000, 8.0, 'b'),
(120000, 9.0, 'b'),
(180000, 10.0, 'b'),
(240000, 11.0, 'b'),
(300000, 12.0, 'b'),
(360000, 13.0, 'b');
-- Host 'a': a counter increasing by 60 per minute, i.e. by 1 per second.
-- Host 'b': a counter starting at 100 and increasing by 30 per minute, i.e. by 0.5 per second. Its
-- rate differs from host 'a', so a window folded for the wrong series cannot pass as the right one.
INSERT INTO at_modifier_counter_total VALUES
(0, 0.0, 'a'),
(60000, 60.0, 'a'),
(120000, 120.0, 'a'),
(180000, 180.0, 'a'),
(240000, 240.0, 'a'),
(300000, 300.0, 'a'),
(360000, 360.0, 'a'),
(0, 100.0, 'b'),
(60000, 130.0, 'b'),
(120000, 160.0, 'b'),
(180000, 190.0, 'b'),
(240000, 220.0, 'b'),
(300000, 250.0, 'b'),
(360000, 280.0, 'b');
-- 1. Instant query anchored at an absolute timestamp: the sample window is `(anchor - lookback,
-- anchor]` = `(0s, 300s]`, so its newest samples are 5.0 for host 'a' and 12.0 for host 'b', both
-- at 300s. The output timestamps stay the evaluation timestamps.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 100, '1s') at_modifier_gauge @ 300;
-- Control: the same query without `@` looks back from the evaluation timestamp instead:
-- `(-200s, 100s]` selects the samples 1.0 (a) and 8.0 (b) at 60s.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 100, '1s') at_modifier_gauge;
-- The anchor, not the evaluation timestamp, decides which sample is reported.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 100, '1s') at_modifier_gauge @ 360;
-- Sub-second precision is preserved in the anchor: `240.999` selects the samples at 240s.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 100, '1s') at_modifier_gauge @ 240.999;
-- The window `(-300s, 0s]` includes the samples exactly at the anchor.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 100, '1s') at_modifier_gauge @ 0;
-- 2. `@ start()`: every step selects its samples around the start of the evaluation range
-- (`(-200s, 100s]`), so every step reports the same value per series (1.0 for 'a', 8.0 for 'b').
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 400, '100s') at_modifier_gauge @ start();
-- Control: without `@` every step looks back from its own timestamp, so the steps differ.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 400, '100s') at_modifier_gauge;
-- 3. `@ end()`: every step selects its samples around the end of the evaluation range
-- (`(100s, 400s]`), whose newest samples are 6.0 (a) and 13.0 (b) at 360s.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 400, '100s') at_modifier_gauge @ end();
-- 4. Range selector anchored by `@`: the call directly above the anchored range selector is
-- evaluated once, at the start of the evaluation, and its result is reported at every step — the
-- planner's counterpart of Prometheus' `StepInvariantExpr`. The window `(0s, 300s]` is left-open,
-- so it holds 60 -> 300 for host 'a' and 130 -> 250 for host 'b', i.e. 240 and 120 units. The 60s
-- gap to the window start is below the extrapolation threshold (1.1 x the 60s sampling interval),
-- so `rate` adds it and divides by the 300s window: 240 * 1.25 / 300 = 1.0 and
-- 120 * 1.25 / 300 = 0.5 per second. Every step must report those same per-series values, and both
-- series must be there at every step.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (0, 240, '60s') rate(at_modifier_counter_total[5m] @ 300);
-- Consistency: the instant query below shows the anchored values over the same left-open
-- `(0s, 300s]` window (1.0 per second for 'a', 0.5 for 'b'). The `!= 1.0` comparison filters
-- host 'a', but host 'b' remains at every step with its rate of 0.5 per second.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (300, 300, '1s') rate(at_modifier_counter_total[5m]);
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (0, 240, '60s') rate(at_modifier_counter_total[5m] @ 300) != 1.0;
-- Parentheses around the range argument are transparent: the call must be promoted exactly like
-- the unparenthesized one above, so every step reports the same anchored values for both series
-- (1.0 for host 'a', 0.5 for host 'b') instead of folding a window per step.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (0, 240, '60s') rate((at_modifier_counter_total[5m] @ 300));
-- The same holds for `@ start()` and `@ end()`, which are also fixed anchors: they resolve to the
-- statement's evaluation range, so a range call using them is evaluated once as well.
-- The statement starts at 60s: the window `(-240s, 60s]` holds only the samples at 0s and 60s, so
-- host 'a' goes 0 -> 60 and host 'b' 100 -> 130. The far 240s gap to the window start is beyond the
-- extrapolation threshold (1.1 x the 60s interval), so only half an interval (30s) is added there;
-- host 'a' starts its counter at 0, which clamps that back: 60 / 300 = 0.2 and
-- 30 * 1.5 / 300 = 0.15 per second.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (60, 300, '60s') rate(at_modifier_counter_total[5m] @ start());
-- The instant query at 60s folds the same window as every step of the query above.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (60, 60, '1s') rate(at_modifier_counter_total[5m]);
-- The statement ends at 240s: the window `(-60s, 240s]` holds the samples 0s to 240s, so host 'a'
-- goes 0 -> 240 and host 'b' 100 -> 220. The 60s gap to the window start is added for both, but
-- host 'a' starts at zero, which clamps it back (240 / 300 = 0.8 per second) while host 'b' keeps
-- it (120 * 1.25 / 300 = 0.5 per second).
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (0, 240, '60s') rate(at_modifier_counter_total[5m] @ end());
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (240, 240, '1s') rate(at_modifier_counter_total[5m]);
-- Control: without `@` each step folds its own window, so the values keep changing.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (0, 240, '60s') rate(at_modifier_counter_total[5m]);
-- `@ start()` on a range selector: the window `(-300s, 0s]` is left-open, so it holds a single
-- sample per series — the one exactly at the anchor — and `count_over_time` returns 1 at every step
-- for both of them.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (0, 300, '100s') count_over_time(at_modifier_counter_total[5m] @ start());
-- Control: without `@` the window keeps moving, so the count grows with the step.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (0, 300, '100s') count_over_time(at_modifier_counter_total[5m]);
-- 5. `@` combined with `offset`: the offset moves the anchor backwards before the window is
-- selected, so `@ 300 offset 2m` reports the samples at 180s.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 100, '1s') at_modifier_gauge @ 300 offset 2m;
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 100, '1s') at_modifier_gauge @ 300 offset 1m;
-- Control: the offset applies to the evaluation timestamp when there is no `@`.
TQL EVAL (100, 100, '1s') at_modifier_gauge offset 2m;
-- 6. `@` inside a larger expression. The anchored operand contributes the same value at every
-- step, while the plain operand still follows the evaluation step. A binary expression is never
-- promoted: when one side is anchored and the other is not, the anchored side replays on its own,
-- and when both sides are anchored, each side reports its own anchored samples. The join then runs
-- at every step over the replayed per-series inputs.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 400, '100s') at_modifier_gauge @ 300 + at_modifier_gauge @ 0;
-- The aggregation above the anchored selector runs at every step over the replayed (still
-- per-series) samples, so it sums both series at every step.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 400, '100s') sum(at_modifier_gauge @ 0);
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 400, '100s') abs(at_modifier_gauge @ 0);
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 400, '100s') at_modifier_gauge @ 300 + at_modifier_gauge;
-- 7. `@ -1`, an anchor before the Unix epoch, is accepted. Its window `(-301s, -1s]` holds no
-- sample, so the result is empty.
TQL EVAL (0, 0, '1s') at_modifier_gauge @ -1;
-- 8. `@` on a metric that does not exist yields an empty result.
TQL EVAL (100, 100, '1s') at_modifier_missing @ 300;
-- The same for a range selector over a missing metric: the anchored window is folded once by the
-- promoted call, but the scan has no row to fold, so the whole grid stays empty.
TQL EVAL (0, 240, '60s') rate(at_modifier_missing[5m] @ 300);
-- 9. Multi-series coverage of the promoted call. The call above the anchored range selector is
-- evaluated once per series and its result is replayed per series, so grouping by `host` must
-- report both groups at every step: 1.0 for host 'a' and 0.5 for host 'b'. A promoted result that
-- treated the two series as one timeline would report the first step from the first row of the
-- batch and the later steps from its last row, dropping or mixing a host here.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (0, 240, '60s') sum by (host) (rate(at_modifier_counter_total[5m] @ 300));
-- Consistency: comparing the per-host sequence above against 1.0 keeps exactly host 'b' at
-- every step, confirming it stays at 0.5 for the whole grid.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (0, 240, '60s') sum by (host) (rate(at_modifier_counter_total[5m] @ 300)) != 1.0;
-- A value function directly over an anchored instant selector is not promoted: the selector
-- anchors and replays its sample per series, and `abs` is evaluated at every step over that replay.
-- It must keep one row per series per step.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (100, 400, '100s') abs(at_modifier_gauge @ 300);
-- 10. `predict_linear` over an anchored range selector: the regression is centered on the
-- evaluation instant of the step, not on the last sample of the window. The window `(0s, 300s]` of
-- host 'a' is left-open (the sample at 0s is outside it) and rises by 1.0 per minute, so each step
-- predicts 60s ahead of the trend read at its own instant: 6.0 at 300s and 7.0 at 360s.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (300, 360, '60s') predict_linear(at_modifier_gauge{host="a"}[5m] @ 300, 60);
-- The anchor and the start of the evaluation differ here, so the window `(-60s, 240s]` — samples 0s
-- to 240s — is shifted by `at_offset` = 300s - 240s = 60s. Every step must follow the evaluation
-- grid (6.0 to 11.0); a regression read at the wrong instant predicts 60s ahead of the window's
-- last sample instead, i.e. 5.0 at every step — the sample at 240s is 4.0 and the trend rises by
-- 1.0 per minute — and shows up as a constant here.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (300, 600, '60s') predict_linear(at_modifier_gauge{host="a"}[5m] @ 240, 60);
-- Control: without `@` every step folds its own window, whose last sample is up to a step older
-- than the evaluation instant. Reading the regression off that sample reports 6.0 / 7.0 instead
-- of 6.5 / 7.5 here.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (330, 390, '60s') predict_linear(at_modifier_gauge{host="a"}[5m], 60);
-- The same holds with an `offset`, which shifts the whole window backwards without moving the
-- evaluation instants: the window of step `T` is the left-open `(T - 1m - 5m, T - 1m]`, and the
-- trend is read at `T`, i.e. one minute past its newest sample.
-- SQLNESS SORT_RESULT 3 1
TQL EVAL (330, 390, '60s') predict_linear(at_modifier_gauge{host="a"}[5m] offset 1m, 60);
-- `timestamp()` of an anchored instant selector is not promoted either: its argument is an
-- instant selector, not a range selector, so the selector anchors and replays its sample per series
-- 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.
-- 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
-- SQLNESS REPLACE (RepartitionExec:.*) RepartitionExec: REDACTED
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 `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.
-- SQLNESS SORT_RESULT 3 1
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", "-", "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", "-", "host", "host");
DROP TABLE at_modifier_gauge;
DROP TABLE at_modifier_counter_total;