prometheus/promql/parser/printer_test.go
György Krajcsovits 2f144dd3ad feat(promql): offset from static time expression
Related to #12318 .

Allow using a PromQL expression for setting the offset, provided that:

the expression evaluates to a scalar
the expression does not contain vector selector (no TSDB access)
the expression does not call the info() function (no TSDB access)
the time() function is only called on the top level of the expression,
  not inside a subquery

We'll call such expression a time expression.

During parsing we no longer set OriginalOffset field in sub-queries and
vector selectors, instead we set a new OriginalOffsetExpr field to the
time expression, even if the expression is a literal number.

Before evaluating the overall PromQL expression, the engine shall
evaluate the time expressions and set the OriginalOffset field in vector
selectors and subqueries to the calculated value. This makes the change
fairly non intrusive. Later we can change the code to start using the
expression more directly. Note: this prohibits using time() function
inside a sub-query as the evaluation time is not constant inside a
sub-query.

Signed-off-by: György Krajcsovits <gyorgy.krajcsovits@grafana.com>
2025-02-03 13:08:35 +01:00

271 lines
5.5 KiB
Go

// Copyright 2015 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"testing"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/model/labels"
)
func TestExprString(t *testing.T) {
// A list of valid expressions that are expected to be
// returned as out when calling String(). If out is empty the output
// is expected to equal the input.
inputs := []struct {
in, out string
}{
{
in: `sum by() (task:errors:rate10s{job="s"})`,
out: `sum(task:errors:rate10s{job="s"})`,
},
{
in: `sum by(code) (task:errors:rate10s{job="s"})`,
out: `sum by (code) (task:errors:rate10s{job="s"})`,
},
{
in: `sum without() (task:errors:rate10s{job="s"})`,
out: `sum without () (task:errors:rate10s{job="s"})`,
},
{
in: `sum without(instance) (task:errors:rate10s{job="s"})`,
out: `sum without (instance) (task:errors:rate10s{job="s"})`,
},
{
in: `sum by("foo.bar") (task:errors:rate10s{job="s"})`,
out: `sum by ("foo.bar") (task:errors:rate10s{job="s"})`,
},
{
in: `sum without("foo.bar") (task:errors:rate10s{job="s"})`,
out: `sum without ("foo.bar") (task:errors:rate10s{job="s"})`,
},
{
in: `topk(5, task:errors:rate10s{job="s"})`,
},
{
in: `count_values("value", task:errors:rate10s{job="s"})`,
},
{
in: `a - on() c`,
out: `a - on () c`,
},
{
in: `a - on(b) c`,
out: `a - on (b) c`,
},
{
in: `a - on(b) group_left(x) c`,
out: `a - on (b) group_left (x) c`,
},
{
in: `a - on(b) group_left(x, y) c`,
out: `a - on (b) group_left (x, y) c`,
},
{
in: `a - on(b) group_left c`,
out: `a - on (b) group_left () c`,
},
{
in: `a - on(b) group_left() (c)`,
out: `a - on (b) group_left () (c)`,
},
{
in: `a - ignoring(b) c`,
out: `a - ignoring (b) c`,
},
{
in: `a - ignoring() c`,
out: `a - c`,
},
{
in: `up > bool 0`,
},
{
in: `a offset 1m`,
},
{
in: `a offset -7m`,
},
{
in: `a{c="d"}[5m] offset 1m`,
},
{
in: `a[5m] offset 1m`,
},
{
in: `a[12m] offset -3m`,
},
{
in: `a[1h:5m] offset 1m`,
},
{
in: `a[12m] offset (1m + 1m)`,
// TODO(krajorama): maybe handle time expressions special for numbers?
out: `a[12m] offset (60 + 60)`,
},
{
in: `{__name__="a"}`,
},
{
in: `a{b!="c"}[1m]`,
},
{
in: `a{b=~"c"}[1m]`,
},
{
in: `a{b!~"c"}[1m]`,
},
{
in: `a @ 10`,
out: `a @ 10.000`,
},
{
in: `a[1m] @ 10`,
out: `a[1m] @ 10.000`,
},
{
in: `a @ start()`,
},
{
in: `a @ end()`,
},
{
in: `a[1m] @ start()`,
},
{
in: `a[1m] @ end()`,
},
{
in: `{__name__="",a="x"}`,
},
{
in: `{"a.b"="c"}`,
},
{
in: `{"0"="1"}`,
},
{
in: `{"_0"="1"}`,
out: `{_0="1"}`,
},
{
in: `{""="0"}`,
},
{
in: "{``=\"0\"}",
out: `{""="0"}`,
},
}
model.NameValidationScheme = model.UTF8Validation
for _, test := range inputs {
expr, err := ParseExpr(test.in)
require.NoError(t, err)
exp := test.in
if test.out != "" {
exp = test.out
}
require.Equal(t, exp, expr.String())
}
}
func TestVectorSelector_String(t *testing.T) {
for _, tc := range []struct {
name string
vs VectorSelector
expected string
}{
{
name: "empty value",
vs: VectorSelector{},
expected: ``,
},
{
name: "no matchers with name",
vs: VectorSelector{Name: "foobar"},
expected: `foobar`,
},
{
name: "one matcher with name",
vs: VectorSelector{
Name: "foobar",
LabelMatchers: []*labels.Matcher{
labels.MustNewMatcher(labels.MatchEqual, "a", "x"),
},
},
expected: `foobar{a="x"}`,
},
{
name: "two matchers with name",
vs: VectorSelector{
Name: "foobar",
LabelMatchers: []*labels.Matcher{
labels.MustNewMatcher(labels.MatchEqual, "a", "x"),
labels.MustNewMatcher(labels.MatchEqual, "b", "y"),
},
},
expected: `foobar{a="x",b="y"}`,
},
{
name: "two matchers without name",
vs: VectorSelector{
LabelMatchers: []*labels.Matcher{
labels.MustNewMatcher(labels.MatchEqual, "a", "x"),
labels.MustNewMatcher(labels.MatchEqual, "b", "y"),
},
},
expected: `{a="x",b="y"}`,
},
{
name: "name matcher and name",
vs: VectorSelector{
Name: "foobar",
LabelMatchers: []*labels.Matcher{
labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "foobar"),
},
},
expected: `foobar`,
},
{
name: "name matcher only",
vs: VectorSelector{
LabelMatchers: []*labels.Matcher{
labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "foobar"),
},
},
expected: `{__name__="foobar"}`,
},
{
name: "empty name matcher",
vs: VectorSelector{
LabelMatchers: []*labels.Matcher{
labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, ""),
labels.MustNewMatcher(labels.MatchEqual, "a", "x"),
},
},
expected: `{__name__="",a="x"}`,
},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.expected, tc.vs.String())
})
}
}