Skip to content

Commit 3f46c44

Browse files
Add ParseWithVars that allows to preserve variables in metricsql expressions
Add `PrettifyExpr` that allows to return prettified string generated from existing expression
1 parent 6ea382c commit 3f46c44

File tree

5 files changed

+142
-20
lines changed

5 files changed

+142
-20
lines changed

lexer.go

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,12 @@ again:
122122
}
123123
goto tokenFoundLabel
124124
}
125-
if strings.HasPrefix(s, "$__interval") {
126-
lex.sTail = s[len("$__interval"):]
127-
return "$__interval", nil
128-
}
129-
if strings.HasPrefix(s, "$__rate_interval") {
130-
lex.sTail = s[len("$__rate_interval"):]
131-
return "$__interval", nil
125+
if isVariable(s) {
126+
token, err = scanVariable(s)
127+
if err != nil {
128+
return "", err
129+
}
130+
goto tokenFoundLabel
132131
}
133132
return "", fmt.Errorf("cannot recognize %q", s)
134133

@@ -301,6 +300,43 @@ func scanPositiveNumber(s string) (string, error) {
301300
return s[:j], nil
302301
}
303302

303+
func isVariable(s string) bool {
304+
return len(s) > 1 && s[0] == '$'
305+
}
306+
307+
func isVariableChar(c byte) bool {
308+
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'
309+
}
310+
311+
func scanVariable(s string) (string, error) {
312+
if len(s) < 2 {
313+
return "", fmt.Errorf("too small string for a variable %q", s)
314+
}
315+
i := 1
316+
hasBraces := s[i] == '{'
317+
if hasBraces {
318+
i++
319+
}
320+
for i < len(s) {
321+
if hasBraces {
322+
if s[i] == '}' {
323+
i++
324+
break
325+
}
326+
if !isVariableChar(s[i]) {
327+
return "", fmt.Errorf("not allowed symbol in variable %q", s)
328+
}
329+
} else if !isVariableChar(s[i]) {
330+
break
331+
}
332+
i++
333+
}
334+
if hasBraces && i <= 3 || i <= 2 {
335+
return "", fmt.Errorf("impossible variable name %q", s)
336+
}
337+
return s[:i], nil
338+
}
339+
304340
func scanNumMultiplier(s string) int {
305341
if len(s) > 3 {
306342
s = s[:3]
@@ -534,7 +570,7 @@ func scanSpecialIntegerPrefix(s string) (skipChars int, isHex bool) {
534570
}
535571

536572
func isPositiveDuration(s string) bool {
537-
if s == "$__interval" {
573+
if s == "$__interval" || s == "$__rate_interval" {
538574
return true
539575
}
540576
n := scanDuration(s)
@@ -610,7 +646,7 @@ func DurationValue(s string, step int64) (int64, error) {
610646
}
611647

612648
func parseSingleDuration(s string, step int64) (float64, error) {
613-
if s == "$__interval" {
649+
if s == "$__interval" || s == "$__rate_interval" {
614650
return float64(step), nil
615651
}
616652

@@ -676,8 +712,8 @@ func scanSingleDuration(s string, canBeNegative bool) int {
676712
if s[0] == '-' && canBeNegative {
677713
i++
678714
}
679-
if s[i:] == "$__interval" {
680-
return i + len("$__interval")
715+
if s[i:] == "$__interval" || s[i:] == "$__rate_interval" {
716+
return i + len(s[i:])
681717
}
682718
for i < len(s) && isDecimalChar(s[i]) {
683719
i++

lexer_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,33 @@ func TestScanPositiveNumberFailure(t *testing.T) {
9898
f("12.34e-")
9999
}
100100

101+
func TestScanVariableSuccess(t *testing.T) {
102+
f := func(s, vsExpected string) {
103+
t.Helper()
104+
vs, err := scanVariable(s)
105+
if err != nil {
106+
t.Fatalf("unexpected error in scanVariable(%q): %s", s, err)
107+
}
108+
if vs != vsExpected {
109+
t.Fatalf("unexpected variable scanned from %q; got %q; want %q", s, vs, vsExpected)
110+
}
111+
}
112+
f("$__rate_interval", "$__rate_interval")
113+
f("${foobar},test", "${foobar}")
114+
}
115+
116+
func TestScanVariableFailure(t *testing.T) {
117+
f := func(s string) {
118+
t.Helper()
119+
vs, err := scanVariable(s)
120+
if err == nil {
121+
t.Fatalf("expecting non-nil error in scanVariable(%q); got result %q", s, vs)
122+
}
123+
}
124+
f("")
125+
f("${foobar,")
126+
}
127+
101128
func TestParsePositiveNumberSuccess(t *testing.T) {
102129
f := func(s string, vExpected float64) {
103130
t.Helper()

parser.go

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ import (
1313
//
1414
// MetricsQL is backwards-compatible with PromQL.
1515
func Parse(s string) (Expr, error) {
16+
return ParseWithVars(s, false)
17+
}
18+
19+
// ParseWithVars provides an option to keep variables in expressions.
20+
func ParseWithVars(s string, keepVars bool) (Expr, error) {
1621
// Parse s
17-
e, err := parseInternal(s)
22+
e, err := parseInternal(s, keepVars)
1823
if err != nil {
1924
return nil, err
2025
}
@@ -32,8 +37,10 @@ func Parse(s string) (Expr, error) {
3237
return e, nil
3338
}
3439

35-
func parseInternal(s string) (Expr, error) {
36-
var p parser
40+
func parseInternal(s string, keepVars bool) (Expr, error) {
41+
p := parser{
42+
keepVars: keepVars,
43+
}
3744
p.lex.Init(s)
3845
if err := p.lex.Next(); err != nil {
3946
return nil, fmt.Errorf(`cannot find the first token: %s`, err)
@@ -258,7 +265,8 @@ func simplifyConstantsInplace(args []Expr) {
258265
// postconditions for all parser.parse* funcs:
259266
// - p.lex.Token should point to the next token after the parsed token.
260267
type parser struct {
261-
lex lexer
268+
lex lexer
269+
keepVars bool
262270
}
263271

264272
func isWith(s string) bool {
@@ -473,6 +481,15 @@ func (p *parser) parseSingleExprWithoutRollupSuffix() (Expr, error) {
473481
if isIdentPrefix(p.lex.Token) {
474482
return p.parseIdentExpr()
475483
}
484+
if isVariable(p.lex.Token) {
485+
e := &NumberExpr{
486+
s: p.lex.Token,
487+
}
488+
if err := p.lex.Next(); err != nil {
489+
return nil, err
490+
}
491+
return e, nil
492+
}
476493
switch p.lex.Token {
477494
case "(":
478495
return p.parseParensExpr()
@@ -1537,7 +1554,12 @@ func (p *parser) parseWindowAndStep() (*DurationExpr, *DurationExpr, bool, error
15371554
}
15381555
var window *DurationExpr
15391556
if !strings.HasPrefix(p.lex.Token, ":") {
1540-
if p.lex.Token == "$__interval" {
1557+
if p.lex.Token == "$__interval" || p.lex.Token == "$__rate_interval" {
1558+
if p.keepVars {
1559+
window = &DurationExpr{
1560+
s: p.lex.Token,
1561+
}
1562+
}
15411563
// Skip $__interval, since it must be treated as missing lookbehind window,
15421564
// e.g. rate(m[$__interval]) must be equivalent to rate(m).
15431565
// In this case VictoriaMetrics automatically adjusts the lookbehind window
@@ -1656,8 +1678,14 @@ func (p *parser) parsePositiveDuration() (*DurationExpr, error) {
16561678
}
16571679
}
16581680
// Verify duration value.
1659-
if s == "$__interval" {
1660-
s = "1i"
1681+
if s == "$__interval" || s == "$__rate_interval" {
1682+
if p.keepVars {
1683+
return &DurationExpr{
1684+
s: s,
1685+
}, nil
1686+
} else {
1687+
s = "1i"
1688+
}
16611689
}
16621690
return newDurationExpr(s)
16631691
}

parser_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func TestParseSuccess(t *testing.T) {
2323
}
2424

2525
// metricExpr
26+
same(`topk($topk, max(sum(vmalert_recording_rules_last_evaluation_samples{job=~"$job",instance=~"$instance",group=~"$group",file=~"$file"}) by(job,instance,group,file,recording) > 0) by(job,group,file,recording))`)
2627
same(`{}`)
2728
same(`{}[5m]`)
2829
same(`{}[5m:]`)
@@ -643,6 +644,31 @@ func TestParseSuccess(t *testing.T) {
643644
another(`rate(m[$__interval:5m])`, `rate(m[:5m])`)
644645
}
645646

647+
func TestParseWithVarsSuccess(t *testing.T) {
648+
another := func(s string, sExpected string) {
649+
t.Helper()
650+
651+
e, err := ParseWithVars(s, true)
652+
if err != nil {
653+
t.Fatalf("unexpected error when parsing %s: %s", s, err)
654+
}
655+
res := e.AppendString(nil)
656+
if string(res) != sExpected {
657+
t.Fatalf("unexpected string constructed;\ngot\n%s\nwant\n%s", res, sExpected)
658+
}
659+
}
660+
same := func(s string) {
661+
t.Helper()
662+
another(s, s)
663+
}
664+
665+
// $__interval and $__rate_interval must be replaced with 1i
666+
same(`rate(m[$__interval] offset $__interval) * $__interval`)
667+
another(`increase(m[$__rate_interval] offset -$__rate_interval) + -$__rate_interval`, `increase(m[$__rate_interval] offset -$__rate_interval) + (0 - $__rate_interval)`)
668+
same(`rate(m[$__rate_interval:5m])`)
669+
same(`rate(m[$__interval:5m])`)
670+
}
671+
646672
func TestParseError(t *testing.T) {
647673
f := func(s string) {
648674
t.Helper()

prettifier.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ package metricsql
22

33
// Prettify returns prettified representation of MetricsQL query q.
44
func Prettify(q string) (string, error) {
5-
e, err := parseInternal(q)
5+
return PrettifyWithVars(q, false)
6+
}
7+
8+
// PrettifyWithVars returns prettified representation of MetricsQL query q and keeps expression variables depending on keepVars value.
9+
func PrettifyWithVars(q string, keepVars bool) (string, error) {
10+
e, err := parseInternal(q, keepVars)
611
if err != nil {
712
return "", err
813
}
914
e = removeParensExpr(e)
1015
b := appendPrettifiedExpr(nil, e, 0, false)
11-
return string(b), nil
16+
return string(b)
1217
}
1318

1419
// maxPrettifiedLineLen is the maximum length of a single line returned by Prettify().

0 commit comments

Comments
 (0)