Merge pull request #18624 from roidelapluie/roidelapluie/dur-expr-in-parse-ast

web/api: emit duration expression trees
This commit is contained in:
Bartlomiej Plotka 2026-05-11 12:59:41 +02:00 committed by GitHub
commit 42003b2e13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 240 additions and 0 deletions

View file

@ -84,7 +84,9 @@ func translateAST(node parser.Expr) any {
"type": "matrixSelector",
"name": vs.Name,
"range": n.Range.Milliseconds(),
"rangeExpr": translateDurationExpr(n.RangeExpr),
"offset": vs.OriginalOffset.Milliseconds(),
"offsetExpr": translateDurationExpr(vs.OriginalOffsetExpr),
"matchers": translateMatchers(vs.LabelMatchers),
"timestamp": vs.Timestamp,
"startOrEnd": getStartOrEnd(vs.StartOrEnd),
@ -96,11 +98,16 @@ func translateAST(node parser.Expr) any {
"type": "subquery",
"expr": translateAST(n.Expr),
"range": n.Range.Milliseconds(),
"rangeExpr": translateDurationExpr(n.RangeExpr),
"offset": n.OriginalOffset.Milliseconds(),
"offsetExpr": translateDurationExpr(n.OriginalOffsetExpr),
"step": n.Step.Milliseconds(),
"stepExpr": translateDurationExpr(n.StepExpr),
"timestamp": n.Timestamp,
"startOrEnd": getStartOrEnd(n.StartOrEnd),
}
case *parser.DurationExpr:
return translateDurationExpr(n)
case *parser.NumberLiteral:
return map[string]string{
"type": "numberLiteral",
@ -127,6 +134,7 @@ func translateAST(node parser.Expr) any {
"type": "vectorSelector",
"name": n.Name,
"offset": n.OriginalOffset.Milliseconds(),
"offsetExpr": translateDurationExpr(n.OriginalOffsetExpr),
"matchers": translateMatchers(n.LabelMatchers),
"timestamp": n.Timestamp,
"startOrEnd": getStartOrEnd(n.StartOrEnd),
@ -137,6 +145,39 @@ func translateAST(node parser.Expr) any {
panic("unsupported node type")
}
func translateDurationExpr(node parser.Expr) any {
if node == nil {
return nil
}
switch n := node.(type) {
case *parser.DurationExpr:
if n == nil {
return nil
}
return map[string]any{
"type": "durationExpr",
"op": n.Op.String(),
"lhs": translateDurationExpr(n.LHS),
"rhs": translateDurationExpr(n.RHS),
"wrapped": n.Wrapped,
}
case *parser.NumberLiteral:
if n == nil {
return nil
}
return map[string]any{
"type": "numberLiteral",
"val": strconv.FormatFloat(n.Val, 'f', -1, 64),
"duration": n.Duration,
}
default:
return translateAST(n)
}
}
func sanitizeList(l []string) []string {
if l == nil {
return []string{}

View file

@ -0,0 +1,199 @@
// Copyright 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 v1
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/promql/parser"
)
func TestTranslateASTDurationExpressions(t *testing.T) {
p := parser.NewParser(parser.Options{})
type tc struct {
name string
query string
wantType string
wantFields map[string]any
}
testcases := []tc{
{
name: "regular matrix selector",
query: `foo[5m]`,
wantType: "matrixSelector",
wantFields: map[string]any{
"range": int64(5 * 60 * 1000),
"rangeExpr": nil,
"offset": int64(0),
"offsetExpr": nil,
},
},
{
name: "regular matrix selector with offset",
query: `foo[5m] offset 1m`,
wantType: "matrixSelector",
wantFields: map[string]any{
"range": int64(5 * 60 * 1000),
"rangeExpr": nil,
"offset": int64(60 * 1000),
"offsetExpr": nil,
},
},
{
name: "matrix selector range expression",
query: `foo[5m+1m]`,
wantType: "matrixSelector",
wantFields: map[string]any{
"range": int64(0),
"rangeExpr": durationExpr("+", durationNumber("300", true), durationNumber("60", true), false),
},
},
{
name: "matrix selector range expression with offset expression",
query: `foo[5m+1m] offset (10m/2)`,
wantType: "matrixSelector",
wantFields: map[string]any{
"rangeExpr": durationExpr("+", durationNumber("300", true), durationNumber("60", true), false),
"offset": int64(0),
"offsetExpr": durationExpr("/", durationNumber("600", true), durationNumber("2", false), true),
},
},
{
name: "complex matrix selector range expression",
query: `foo[max(step(),5m+3m) ]`,
wantType: "matrixSelector",
wantFields: map[string]any{
"range": int64(0),
"rangeExpr": durationExpr("max",
durationExpr("step", nil, nil, false),
durationExpr("+", durationNumber("300", true), durationNumber("180", true), false),
false,
),
},
},
{
name: "nested min and max matrix selector range expression",
query: `foo[min(max(step(),5m+3m),10m-2m)]`,
wantType: "matrixSelector",
wantFields: map[string]any{
"range": int64(0),
"rangeExpr": durationExpr("min",
durationExpr("max",
durationExpr("step", nil, nil, false),
durationExpr("+", durationNumber("300", true), durationNumber("180", true), false),
false,
),
durationExpr("-", durationNumber("600", true), durationNumber("120", true), false),
false,
),
},
},
{
name: "range preprocessor expression",
query: `foo[range()]`,
wantType: "matrixSelector",
wantFields: map[string]any{
"range": int64(0),
"rangeExpr": durationExpr("range", nil, nil, false),
},
},
{
name: "regular subquery selector",
query: `foo[5m:1m]`,
wantType: "subquery",
wantFields: map[string]any{
"range": int64(5 * 60 * 1000),
"rangeExpr": nil,
"step": int64(60 * 1000),
"stepExpr": nil,
"offset": int64(0),
"offsetExpr": nil,
},
},
{
name: "subquery selector duration expressions",
query: `foo[4s+4s:1s*2] offset (5s-8)`,
wantType: "subquery",
wantFields: map[string]any{
"range": int64(0),
"rangeExpr": durationExpr("+", durationNumber("4", true), durationNumber("4", true), false),
"step": int64(0),
"stepExpr": durationExpr("*", durationNumber("1", true), durationNumber("2", false), false),
"offset": int64(0),
"offsetExpr": durationExpr("-", durationNumber("5", true), durationNumber("8", false), true),
},
},
{
name: "regular vector selector offset",
query: `foo offset 5m`,
wantType: "vectorSelector",
wantFields: map[string]any{
"offset": int64(5 * 60 * 1000),
"offsetExpr": nil,
},
},
{
name: "vector selector offset expression",
query: `foo offset -min(5s,step()+8s)`,
wantType: "vectorSelector",
wantFields: map[string]any{
"offset": int64(0),
"offsetExpr": durationExpr("-", nil,
durationExpr("min",
durationNumber("5", true),
durationExpr("+", durationExpr("step", nil, nil, false), durationNumber("8", true), false),
false,
),
false,
),
},
},
}
for _, tcase := range testcases {
t.Run(tcase.name, func(t *testing.T) {
expr, err := p.ParseExpr(tcase.query)
require.NoError(t, err)
got := translateAST(expr).(map[string]any)
require.Equal(t, tcase.wantType, got["type"])
for field, want := range tcase.wantFields {
require.Contains(t, got, field)
require.Equal(t, want, got[field], field)
}
})
}
}
func durationExpr(op string, lhs, rhs any, wrapped bool) map[string]any {
return map[string]any{
"type": "durationExpr",
"op": op,
"lhs": lhs,
"rhs": rhs,
"wrapped": wrapped,
}
}
func durationNumber(val string, duration bool) map[string]any {
return map[string]any{
"type": "numberLiteral",
"val": val,
"duration": duration,
}
}