diff --git a/promql/parser/generated_parser.y b/promql/parser/generated_parser.y index 6e336e230b..cc9104cdb2 100644 --- a/promql/parser/generated_parser.y +++ b/promql/parser/generated_parser.y @@ -599,6 +599,13 @@ matrix_selector : expr LEFT_BRACKET positive_duration_expr RIGHT_BRACKET subquery_expr : expr LEFT_BRACKET positive_duration_expr COLON positive_duration_expr RIGHT_BRACKET { + // Check if offset/@ modifiers are immediately before the subquery brackets. + hasModifiers, errMsg := yylex.(*parser).hasModifiersBeforeRange($1.(Expr), $2.Pos) + if hasModifiers { + errRange := mergeRanges(&$2, &$6) + yylex.(*parser).addParseErrf(errRange, "%s", errMsg) + } + var rangeNl time.Duration var stepNl time.Duration if numLit, ok := $3.(*NumberLiteral); ok { @@ -620,6 +627,13 @@ subquery_expr : expr LEFT_BRACKET positive_duration_expr COLON positive_durati } | expr LEFT_BRACKET positive_duration_expr COLON RIGHT_BRACKET { + // Check if offset/@ modifiers are immediately before the subquery brackets. + hasModifiers, errMsg := yylex.(*parser).hasModifiersBeforeRange($1.(Expr), $2.Pos) + if hasModifiers { + errRange := mergeRanges(&$2, &$5) + yylex.(*parser).addParseErrf(errRange, "%s", errMsg) + } + var rangeNl time.Duration if numLit, ok := $3.(*NumberLiteral); ok { rangeNl = time.Duration(math.Round(numLit.Val*float64(time.Second))) diff --git a/promql/parser/generated_parser.y.go b/promql/parser/generated_parser.y.go index 4b90d757cf..23b431770a 100644 --- a/promql/parser/generated_parser.y.go +++ b/promql/parser/generated_parser.y.go @@ -1594,6 +1594,13 @@ yydefault: case 90: yyDollar = yyS[yypt-6 : yypt+1] { + // Check if offset/@ modifiers are immediately before the subquery brackets. + hasModifiers, errMsg := yylex.(*parser).hasModifiersBeforeRange(yyDollar[1].node.(Expr), yyDollar[2].item.Pos) + if hasModifiers { + errRange := mergeRanges(&yyDollar[2].item, &yyDollar[6].item) + yylex.(*parser).addParseErrf(errRange, "%s", errMsg) + } + var rangeNl time.Duration var stepNl time.Duration if numLit, ok := yyDollar[3].node.(*NumberLiteral); ok { @@ -1616,6 +1623,13 @@ yydefault: case 91: yyDollar = yyS[yypt-5 : yypt+1] { + // Check if offset/@ modifiers are immediately before the subquery brackets. + hasModifiers, errMsg := yylex.(*parser).hasModifiersBeforeRange(yyDollar[1].node.(Expr), yyDollar[2].item.Pos) + if hasModifiers { + errRange := mergeRanges(&yyDollar[2].item, &yyDollar[5].item) + yylex.(*parser).addParseErrf(errRange, "%s", errMsg) + } + var rangeNl time.Duration if numLit, ok := yyDollar[3].node.(*NumberLiteral); ok { rangeNl = time.Duration(math.Round(numLit.Val * float64(time.Second))) diff --git a/promql/parser/parse.go b/promql/parser/parse.go index cefc627fda..25d34f93bf 100644 --- a/promql/parser/parse.go +++ b/promql/parser/parse.go @@ -1191,6 +1191,49 @@ func (p *parser) getAtModifierVars(e Node) (**int64, *ItemType, *posrange.Pos, b return timestampp, preprocp, endPosp, true } +// hasModifiersBeforeRange checks if an expression has offset/@ modifiers that are +// immediately adjacent to a bracket (no space), which should be rejected for subqueries. +func (*parser) hasModifiersBeforeRange(expr Expr, bracketPos posrange.Pos) (bool, string) { + var endPos posrange.Pos + var hasOffset bool + var hasAtModifier bool + + switch e := expr.(type) { + case *VectorSelector: + endPos = e.PosRange.End + hasOffset = e.OriginalOffset != 0 || e.OriginalOffsetExpr != nil + hasAtModifier = e.Timestamp != nil + case *MatrixSelector: + vs, ok := e.VectorSelector.(*VectorSelector) + if !ok { + return false, "" + } + endPos = e.EndPos + hasOffset = vs.OriginalOffset != 0 || vs.OriginalOffsetExpr != nil + hasAtModifier = vs.Timestamp != nil + case *SubqueryExpr: + endPos = e.EndPos + hasOffset = e.OriginalOffset != 0 || e.OriginalOffsetExpr != nil + hasAtModifier = e.Timestamp != nil + default: + // Other expression types (function calls, binary ops, etc.) don't have modifiers + // at the selector level, so they're fine. + return false, "" + } + + // Check if modifiers exist and are adjacent to the bracket (no space) + if endPos == bracketPos { + if hasOffset { + return true, "no offset modifiers allowed before range" + } + if hasAtModifier { + return true, "no @ modifiers allowed before range" + } + } + + return false, "" +} + func (p *parser) experimentalDurationExpr(e Expr) { if !ExperimentalDurationExpr { p.addParseErrf(e.PositionRange(), "experimental duration expression is not enabled") diff --git a/promql/parser/parse_test.go b/promql/parser/parse_test.go index ab5564f0ff..4ad69ea76a 100644 --- a/promql/parser/parse_test.go +++ b/promql/parser/parse_test.go @@ -2779,6 +2779,75 @@ var testExpr = []struct { }, }, }, + // Test subquery: offset/@ modifiers immediately before subquery brackets (should error) + { + input: `some_metric offset 5m[2m:10s]`, + fail: true, + errors: ParseErrors{ + ParseErr{ + PositionRange: posrange.PositionRange{Start: 21, End: 29}, + Err: errors.New("no offset modifiers allowed before range"), + Query: `some_metric offset 5m[2m:10s]`, + }, + }, + }, + { + input: `some_metric @ 123[2m:10s]`, + fail: true, + errors: ParseErrors{ + ParseErr{ + PositionRange: posrange.PositionRange{Start: 17, End: 25}, + Err: errors.New("no @ modifiers allowed before range"), + Query: `some_metric @ 123[2m:10s]`, + }, + }, + }, + // Test subquery: matrix selector with offset/@ before subquery (should error) + { + input: `some_metric[5m] offset 1m[2m:10s]`, + fail: true, + errors: ParseErrors{ + ParseErr{ + PositionRange: posrange.PositionRange{Start: 25, End: 33}, + Err: errors.New("no offset modifiers allowed before range"), + Query: `some_metric[5m] offset 1m[2m:10s]`, + }, + }, + }, + { + input: `some_metric[5m] @ 123[2m:10s]`, + fail: true, + errors: ParseErrors{ + ParseErr{ + PositionRange: posrange.PositionRange{Start: 21, End: 29}, + Err: errors.New("no @ modifiers allowed before range"), + Query: `some_metric[5m] @ 123[2m:10s]`, + }, + }, + }, + // Test subquery: nested subquery with offset/@ before outer subquery (should error) + { + input: `some_metric[2m:10s] offset 1m[5m:1m]`, + fail: true, + errors: ParseErrors{ + ParseErr{ + PositionRange: posrange.PositionRange{Start: 29, End: 36}, + Err: errors.New("no offset modifiers allowed before range"), + Query: `some_metric[2m:10s] offset 1m[5m:1m]`, + }, + }, + }, + { + input: `some_metric[2m:10s] @ 123[5m:1m]`, + fail: true, + errors: ParseErrors{ + ParseErr{ + PositionRange: posrange.PositionRange{Start: 25, End: 32}, + Err: errors.New("no @ modifiers allowed before range"), + Query: `some_metric[2m:10s] @ 123[5m:1m]`, + }, + }, + }, { input: `(foo + bar)[5m]`, fail: true,