Merge pull request #18623 from roidelapluie/promql-parser-range-duration-keyword

promql/parser: recognize range in duration expressions
This commit is contained in:
Julien 2026-05-06 13:45:45 +02:00 committed by GitHub
commit bd4758a835
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 60 additions and 20 deletions

View File

@ -499,15 +499,15 @@ func lexStatements(l *Lexer) stateFn {
l.backup()
return lexKeywordOrIdentifier
}
switch r {
case ':':
switch {
case r == ':':
if l.gotColon {
return l.errorf("unexpected colon %q", r)
}
l.emit(COLON)
l.gotColon = true
return lexStatements
case 's', 'S', 'm', 'M':
case isDurationKeywordStartChar(r):
if l.scanDurationKeyword() {
return lexStatements
}
@ -935,6 +935,32 @@ func lexNumber(l *Lexer) stateFn {
return lexStatements
}
// durationKeywordTokens maps lowercase duration keyword names to their token types.
var durationKeywordTokens = map[string]ItemType{
"step": STEP,
"range": RANGE,
"min": MIN,
"max": MAX,
}
// durationKeywordStartChars is the set of lowercase runes that can start a duration keyword,
// derived from durationKeywordTokens.
var durationKeywordStartChars = makeDurationKeywordStartChars()
func makeDurationKeywordStartChars() map[rune]struct{} {
m := make(map[rune]struct{}, len(durationKeywordTokens))
for kw := range durationKeywordTokens {
m[rune(kw[0])] = struct{}{}
}
return m
}
// isDurationKeywordStartChar reports whether r can be the first character of a duration keyword.
func isDurationKeywordStartChar(r rune) bool {
_, ok := durationKeywordStartChars[unicode.ToLower(r)]
return ok
}
func (l *Lexer) scanDurationKeyword() bool {
for {
switch r := l.next(); {
@ -942,24 +968,12 @@ func (l *Lexer) scanDurationKeyword() bool {
// absorb.
default:
l.backup()
word := l.input[l.start:l.pos]
kw := strings.ToLower(word)
switch kw {
case "step":
l.emit(STEP)
word := strings.ToLower(l.input[l.start:l.pos])
if tok, ok := durationKeywordTokens[word]; ok {
l.emit(tok)
return true
case "range":
l.emit(RANGE)
return true
case "min":
l.emit(MIN)
return true
case "max":
l.emit(MAX)
return true
default:
return false
}
return false
}
}
}
@ -1239,7 +1253,7 @@ func lexDurationExpr(l *Lexer) stateFn {
case r == ',':
l.emit(COMMA)
return lexDurationExpr
case r == 's' || r == 'S' || r == 'm' || r == 'M' || r == 'r' || r == 'R':
case isDurationKeywordStartChar(r):
if l.scanDurationKeyword() {
return lexDurationExpr
}

View File

@ -4718,6 +4718,32 @@ var testExpr = []struct {
EndPos: 12,
},
},
{
input: `foo[2m/range()]`,
expected: &MatrixSelector{
VectorSelector: &VectorSelector{
Name: "foo",
LabelMatchers: []*labels.Matcher{
MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
},
PosRange: posrange.PositionRange{Start: 0, End: 3},
},
RangeExpr: &DurationExpr{
Op: DIV,
LHS: &NumberLiteral{
Val: 120,
Duration: true,
PosRange: posrange.PositionRange{Start: 4, End: 6},
},
RHS: &DurationExpr{
Op: RANGE,
StartPos: 7,
EndPos: 14,
},
},
EndPos: 15,
},
},
{
input: `foo[-range()]`,
expected: &MatrixSelector{