Split Extended PromQL parsing to a separate library

This commit is contained in:
Mike Poindexter 2019-12-08 14:52:31 -08:00 committed by Aliaksandr Valialkin
parent ff18101d30
commit 009d1559db
30 changed files with 2504 additions and 2020 deletions

View File

@ -8,6 +8,7 @@ import (
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/VictoriaMetrics/metrics"
"github.com/valyala/histogram"
@ -48,7 +49,7 @@ type aggrFunc func(afa *aggrFuncArg) ([]*timeseries, error)
type aggrFuncArg struct {
args [][]*timeseries
ae *aggrFuncExpr
ae *promql.AggrFuncExpr
ec *EvalConfig
}
@ -57,20 +58,6 @@ func getAggrFunc(s string) aggrFunc {
return aggrFuncs[s]
}
func isAggrFunc(s string) bool {
return getAggrFunc(s) != nil
}
func isAggrFuncModifier(s string) bool {
s = strings.ToLower(s)
switch s {
case "by", "without":
return true
default:
return false
}
}
func newAggrFunc(afe func(tss []*timeseries) []*timeseries) aggrFunc {
return func(afa *aggrFuncArg) ([]*timeseries, error) {
args := afa.args
@ -81,7 +68,7 @@ func newAggrFunc(afe func(tss []*timeseries) []*timeseries) aggrFunc {
}
}
func removeGroupTags(metricName *storage.MetricName, modifier *modifierExpr) {
func removeGroupTags(metricName *storage.MetricName, modifier *promql.ModifierExpr) {
groupOp := strings.ToLower(modifier.Op)
switch groupOp {
case "", "by":
@ -93,7 +80,7 @@ func removeGroupTags(metricName *storage.MetricName, modifier *modifierExpr) {
}
}
func aggrFuncExt(afe func(tss []*timeseries) []*timeseries, argOrig []*timeseries, modifier *modifierExpr, keepOriginal bool) ([]*timeseries, error) {
func aggrFuncExt(afe func(tss []*timeseries) []*timeseries, argOrig []*timeseries, modifier *promql.ModifierExpr, keepOriginal bool) ([]*timeseries, error) {
arg := copyTimeseriesMetricNames(argOrig)
// Perform grouping.

View File

@ -4,6 +4,8 @@ import (
"math"
"strings"
"sync"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
)
// callbacks for optimized incremental calculations for aggregate functions
@ -49,7 +51,7 @@ var incrementalAggrFuncCallbacksMap = map[string]*incrementalAggrFuncCallbacks{
}
type incrementalAggrFuncContext struct {
ae *aggrFuncExpr
ae *promql.AggrFuncExpr
mLock sync.Mutex
m map[uint]map[string]*incrementalAggrContext
@ -57,7 +59,7 @@ type incrementalAggrFuncContext struct {
callbacks *incrementalAggrFuncCallbacks
}
func newIncrementalAggrFuncContext(ae *aggrFuncExpr, callbacks *incrementalAggrFuncCallbacks) *incrementalAggrFuncContext {
func newIncrementalAggrFuncContext(ae *promql.AggrFuncExpr, callbacks *incrementalAggrFuncCallbacks) *incrementalAggrFuncContext {
return &incrementalAggrFuncContext{
ae: ae,
m: make(map[uint]map[string]*incrementalAggrContext),

View File

@ -7,6 +7,8 @@ import (
"runtime"
"sync"
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
)
func TestIncrementalAggr(t *testing.T) {
@ -42,7 +44,7 @@ func TestIncrementalAggr(t *testing.T) {
f := func(name string, valuesExpected []float64) {
t.Helper()
callbacks := getIncrementalAggrFuncCallbacks(name)
ae := &aggrFuncExpr{
ae := &promql.AggrFuncExpr{
Name: name,
}
tssExpected := []*timeseries{{

View File

@ -0,0 +1,9 @@
package promql
import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
)
// DurationValue returns the duration in milliseconds for the given s
// and the given step.
var DurationValue = promql.DurationValue

View File

@ -6,6 +6,7 @@ import (
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
@ -36,94 +37,11 @@ var binaryOpFuncs = map[string]binaryOpFunc{
"default": newBinaryOpArithFunc(binaryOpDefault),
}
var binaryOpPriorities = map[string]int{
"default": -1,
"if": 0,
"ifnot": 0,
// See https://prometheus.io/docs/prometheus/latest/querying/operators/#binary-operator-precedence
"or": 1,
"and": 2,
"unless": 2,
"==": 3,
"!=": 3,
"<": 3,
">": 3,
"<=": 3,
">=": 3,
"+": 4,
"-": 4,
"*": 5,
"/": 5,
"%": 5,
"^": 6,
}
func getBinaryOpFunc(op string) binaryOpFunc {
op = strings.ToLower(op)
return binaryOpFuncs[op]
}
func isBinaryOp(op string) bool {
return getBinaryOpFunc(op) != nil
}
func binaryOpPriority(op string) int {
op = strings.ToLower(op)
return binaryOpPriorities[op]
}
func scanBinaryOpPrefix(s string) int {
n := 0
for op := range binaryOpFuncs {
if len(s) < len(op) {
continue
}
ss := strings.ToLower(s[:len(op)])
if ss == op && len(op) > n {
n = len(op)
}
}
return n
}
func isRightAssociativeBinaryOp(op string) bool {
// See https://prometheus.io/docs/prometheus/latest/querying/operators/#binary-operator-precedence
return op == "^"
}
func isBinaryOpGroupModifier(s string) bool {
s = strings.ToLower(s)
switch s {
// See https://prometheus.io/docs/prometheus/latest/querying/operators/#vector-matching
case "on", "ignoring":
return true
default:
return false
}
}
func isBinaryOpJoinModifier(s string) bool {
s = strings.ToLower(s)
switch s {
case "group_left", "group_right":
return true
default:
return false
}
}
func isBinaryOpBoolModifier(s string) bool {
s = strings.ToLower(s)
return s == "bool"
}
func isBinaryOpCmp(op string) bool {
switch op {
case "==", "!=", ">", "<", ">=", "<=":
@ -133,16 +51,6 @@ func isBinaryOpCmp(op string) bool {
}
}
func isBinaryOpLogicalSet(op string) bool {
op = strings.ToLower(op)
switch op {
case "and", "or", "unless":
return true
default:
return false
}
}
func binaryOpConstants(op string, left, right float64, isBool bool) float64 {
if isBinaryOpCmp(op) {
evalCmp := func(cf func(left, right float64) bool) float64 {
@ -207,7 +115,7 @@ func binaryOpConstants(op string, left, right float64, isBool bool) float64 {
}
type binaryOpFuncArg struct {
be *binaryOpExpr
be *promql.BinaryOpExpr
left []*timeseries
right []*timeseries
}
@ -267,7 +175,7 @@ func newBinaryOpFunc(bf func(left, right float64, isBool bool) float64) binaryOp
}
}
func adjustBinaryOpTags(be *binaryOpExpr, left, right []*timeseries) ([]*timeseries, []*timeseries, []*timeseries, error) {
func adjustBinaryOpTags(be *promql.BinaryOpExpr, left, right []*timeseries) ([]*timeseries, []*timeseries, []*timeseries, error) {
if len(be.GroupModifier.Op) == 0 && len(be.JoinModifier.Op) == 0 {
if isScalar(left) {
// Fast path: `scalar op vector`
@ -348,7 +256,7 @@ func adjustBinaryOpTags(be *binaryOpExpr, left, right []*timeseries) ([]*timeser
return rvsLeft, rvsRight, dst, nil
}
func ensureSingleTimeseries(side string, be *binaryOpExpr, tss []*timeseries) error {
func ensureSingleTimeseries(side string, be *promql.BinaryOpExpr, tss []*timeseries) error {
if len(tss) == 0 {
logger.Panicf("BUG: tss must contain at least one value")
}
@ -362,7 +270,7 @@ func ensureSingleTimeseries(side string, be *binaryOpExpr, tss []*timeseries) er
return nil
}
func groupJoin(singleTimeseriesSide string, be *binaryOpExpr, rvsLeft, rvsRight, tssLeft, tssRight []*timeseries) ([]*timeseries, []*timeseries, error) {
func groupJoin(singleTimeseriesSide string, be *promql.BinaryOpExpr, rvsLeft, rvsRight, tssLeft, tssRight []*timeseries) ([]*timeseries, []*timeseries, error) {
joinTags := be.JoinModifier.Args
var m map[string]*timeseries
for _, tsLeft := range tssLeft {
@ -432,7 +340,7 @@ func mergeNonOverlappingTimeseries(dst, src *timeseries) bool {
return true
}
func resetMetricGroupIfRequired(be *binaryOpExpr, ts *timeseries) {
func resetMetricGroupIfRequired(be *promql.BinaryOpExpr, ts *timeseries) {
if isBinaryOpCmp(be.Op) && !be.Bool {
// Do not reset MetricGroup for non-boolean `compare` binary ops like Prometheus does.
return
@ -565,7 +473,7 @@ func binaryOpUnless(bfa *binaryOpFuncArg) ([]*timeseries, error) {
return rvs, nil
}
func createTimeseriesMapByTagSet(be *binaryOpExpr, left, right []*timeseries) (map[string][]*timeseries, map[string][]*timeseries) {
func createTimeseriesMapByTagSet(be *promql.BinaryOpExpr, left, right []*timeseries) (map[string][]*timeseries, map[string][]*timeseries) {
groupTags := be.GroupModifier.Args
groupOp := strings.ToLower(be.GroupModifier.Op)
if len(groupOp) == 0 {

View File

@ -6,12 +6,14 @@ import (
"math"
"runtime"
"sync"
"unsafe"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/VictoriaMetrics/metrics"
)
@ -158,9 +160,9 @@ func getTimestamps(start, end, step int64) []int64 {
return timestamps
}
func evalExpr(ec *EvalConfig, e expr) ([]*timeseries, error) {
if me, ok := e.(*metricExpr); ok {
re := &rollupExpr{
func evalExpr(ec *EvalConfig, e promql.Expr) ([]*timeseries, error) {
if me, ok := e.(*promql.MetricExpr); ok {
re := &promql.RollupExpr{
Expr: me,
}
rv, err := evalRollupFunc(ec, "default_rollup", rollupDefault, re, nil)
@ -169,14 +171,14 @@ func evalExpr(ec *EvalConfig, e expr) ([]*timeseries, error) {
}
return rv, nil
}
if re, ok := e.(*rollupExpr); ok {
if re, ok := e.(*promql.RollupExpr); ok {
rv, err := evalRollupFunc(ec, "default_rollup", rollupDefault, re, nil)
if err != nil {
return nil, fmt.Errorf(`cannot evaluate %q: %s`, re.AppendString(nil), err)
}
return rv, nil
}
if fe, ok := e.(*funcExpr); ok {
if fe, ok := e.(*promql.FuncExpr); ok {
nrf := getRollupFunc(fe.Name)
if nrf == nil {
args, err := evalExprs(ec, fe.Args)
@ -212,7 +214,7 @@ func evalExpr(ec *EvalConfig, e expr) ([]*timeseries, error) {
}
return rv, nil
}
if ae, ok := e.(*aggrFuncExpr); ok {
if ae, ok := e.(*promql.AggrFuncExpr); ok {
if callbacks := getIncrementalAggrFuncCallbacks(ae.Name); callbacks != nil {
fe, nrf := tryGetArgRollupFuncWithMetricExpr(ae)
if fe != nil {
@ -249,7 +251,7 @@ func evalExpr(ec *EvalConfig, e expr) ([]*timeseries, error) {
}
return rv, nil
}
if be, ok := e.(*binaryOpExpr); ok {
if be, ok := e.(*promql.BinaryOpExpr); ok {
left, err := evalExpr(ec, be.Left)
if err != nil {
return nil, err
@ -273,18 +275,18 @@ func evalExpr(ec *EvalConfig, e expr) ([]*timeseries, error) {
}
return rv, nil
}
if ne, ok := e.(*numberExpr); ok {
if ne, ok := e.(*promql.NumberExpr); ok {
rv := evalNumber(ec, ne.N)
return rv, nil
}
if se, ok := e.(*stringExpr); ok {
if se, ok := e.(*promql.StringExpr); ok {
rv := evalString(ec, se.S)
return rv, nil
}
return nil, fmt.Errorf("unexpected expression %q", e.AppendString(nil))
}
func tryGetArgRollupFuncWithMetricExpr(ae *aggrFuncExpr) (*funcExpr, newRollupFunc) {
func tryGetArgRollupFuncWithMetricExpr(ae *promql.AggrFuncExpr) (*promql.FuncExpr, newRollupFunc) {
if len(ae.Args) != 1 {
return nil, nil
}
@ -295,31 +297,31 @@ func tryGetArgRollupFuncWithMetricExpr(ae *aggrFuncExpr) (*funcExpr, newRollupFu
// - rollupFunc(metricExpr)
// - rollupFunc(metricExpr[d])
if me, ok := e.(*metricExpr); ok {
if me, ok := e.(*promql.MetricExpr); ok {
// e = metricExpr
if me.IsEmpty() {
return nil, nil
}
fe := &funcExpr{
fe := &promql.FuncExpr{
Name: "default_rollup",
Args: []expr{me},
Args: []promql.Expr{me},
}
nrf := getRollupFunc(fe.Name)
return fe, nrf
}
if re, ok := e.(*rollupExpr); ok {
if me, ok := re.Expr.(*metricExpr); !ok || me.IsEmpty() || re.ForSubquery() {
if re, ok := e.(*promql.RollupExpr); ok {
if me, ok := re.Expr.(*promql.MetricExpr); !ok || me.IsEmpty() || re.ForSubquery() {
return nil, nil
}
// e = metricExpr[d]
fe := &funcExpr{
fe := &promql.FuncExpr{
Name: "default_rollup",
Args: []expr{re},
Args: []promql.Expr{re},
}
nrf := getRollupFunc(fe.Name)
return fe, nrf
}
fe, ok := e.(*funcExpr)
fe, ok := e.(*promql.FuncExpr)
if !ok {
return nil, nil
}
@ -329,18 +331,18 @@ func tryGetArgRollupFuncWithMetricExpr(ae *aggrFuncExpr) (*funcExpr, newRollupFu
}
rollupArgIdx := getRollupArgIdx(fe.Name)
arg := fe.Args[rollupArgIdx]
if me, ok := arg.(*metricExpr); ok {
if me, ok := arg.(*promql.MetricExpr); ok {
if me.IsEmpty() {
return nil, nil
}
// e = rollupFunc(metricExpr)
return &funcExpr{
return &promql.FuncExpr{
Name: fe.Name,
Args: []expr{me},
Args: []promql.Expr{me},
}, nrf
}
if re, ok := arg.(*rollupExpr); ok {
if me, ok := re.Expr.(*metricExpr); !ok || me.IsEmpty() || re.ForSubquery() {
if re, ok := arg.(*promql.RollupExpr); ok {
if me, ok := re.Expr.(*promql.MetricExpr); !ok || me.IsEmpty() || re.ForSubquery() {
return nil, nil
}
// e = rollupFunc(metricExpr[d])
@ -349,7 +351,7 @@ func tryGetArgRollupFuncWithMetricExpr(ae *aggrFuncExpr) (*funcExpr, newRollupFu
return nil, nil
}
func evalExprs(ec *EvalConfig, es []expr) ([][]*timeseries, error) {
func evalExprs(ec *EvalConfig, es []promql.Expr) ([][]*timeseries, error) {
var rvs [][]*timeseries
for _, e := range es {
rv, err := evalExpr(ec, e)
@ -361,8 +363,8 @@ func evalExprs(ec *EvalConfig, es []expr) ([][]*timeseries, error) {
return rvs, nil
}
func evalRollupFuncArgs(ec *EvalConfig, fe *funcExpr) ([]interface{}, *rollupExpr, error) {
var re *rollupExpr
func evalRollupFuncArgs(ec *EvalConfig, fe *promql.FuncExpr) ([]interface{}, *promql.RollupExpr, error) {
var re *promql.RollupExpr
rollupArgIdx := getRollupArgIdx(fe.Name)
args := make([]interface{}, len(fe.Args))
for i, arg := range fe.Args {
@ -380,11 +382,11 @@ func evalRollupFuncArgs(ec *EvalConfig, fe *funcExpr) ([]interface{}, *rollupExp
return args, re, nil
}
func getRollupExprArg(arg expr) *rollupExpr {
re, ok := arg.(*rollupExpr)
func getRollupExprArg(arg promql.Expr) *promql.RollupExpr {
re, ok := arg.(*promql.RollupExpr)
if !ok {
// Wrap non-rollup arg into rollupExpr.
return &rollupExpr{
return &promql.RollupExpr{
Expr: arg,
}
}
@ -392,28 +394,28 @@ func getRollupExprArg(arg expr) *rollupExpr {
// Return standard rollup if it doesn't contain subquery.
return re
}
me, ok := re.Expr.(*metricExpr)
me, ok := re.Expr.(*promql.MetricExpr)
if !ok {
// arg contains subquery.
return re
}
// Convert me[w:step] -> default_rollup(me)[w:step]
reNew := *re
reNew.Expr = &funcExpr{
reNew.Expr = &promql.FuncExpr{
Name: "default_rollup",
Args: []expr{
&rollupExpr{Expr: me},
Args: []promql.Expr{
&promql.RollupExpr{Expr: me},
},
}
return &reNew
}
func evalRollupFunc(ec *EvalConfig, name string, rf rollupFunc, re *rollupExpr, iafc *incrementalAggrFuncContext) ([]*timeseries, error) {
func evalRollupFunc(ec *EvalConfig, name string, rf rollupFunc, re *promql.RollupExpr, iafc *incrementalAggrFuncContext) ([]*timeseries, error) {
ecNew := ec
var offset int64
if len(re.Offset) > 0 {
var err error
offset, err = DurationValue(re.Offset, ec.Step)
offset, err = promql.DurationValue(re.Offset, ec.Step)
if err != nil {
return nil, err
}
@ -429,7 +431,7 @@ func evalRollupFunc(ec *EvalConfig, name string, rf rollupFunc, re *rollupExpr,
}
var rvs []*timeseries
var err error
if me, ok := re.Expr.(*metricExpr); ok {
if me, ok := re.Expr.(*promql.MetricExpr); ok {
rvs, err = evalRollupFuncWithMetricExpr(ecNew, name, rf, me, iafc, re.Window)
} else {
if iafc != nil {
@ -454,12 +456,12 @@ func evalRollupFunc(ec *EvalConfig, name string, rf rollupFunc, re *rollupExpr,
return rvs, nil
}
func evalRollupFuncWithSubquery(ec *EvalConfig, name string, rf rollupFunc, re *rollupExpr) ([]*timeseries, error) {
func evalRollupFuncWithSubquery(ec *EvalConfig, name string, rf rollupFunc, re *promql.RollupExpr) ([]*timeseries, error) {
// Do not use rollupResultCacheV here, since it works only with metricExpr.
var step int64
if len(re.Step) > 0 {
var err error
step, err = PositiveDurationValue(re.Step, ec.Step)
step, err = promql.DurationValue(re.Step, ec.Step)
if err != nil {
return nil, err
}
@ -469,7 +471,7 @@ func evalRollupFuncWithSubquery(ec *EvalConfig, name string, rf rollupFunc, re *
var window int64
if len(re.Window) > 0 {
var err error
window, err = PositiveDurationValue(re.Window, ec.Step)
window, err = promql.DurationValue(re.Window, ec.Step)
if err != nil {
return nil, err
}
@ -563,14 +565,14 @@ var (
rollupResultCacheMiss = metrics.NewCounter(`vm_rollup_result_cache_miss_total`)
)
func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, me *metricExpr, iafc *incrementalAggrFuncContext, windowStr string) ([]*timeseries, error) {
func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, me *promql.MetricExpr, iafc *incrementalAggrFuncContext, windowStr string) ([]*timeseries, error) {
if me.IsEmpty() {
return evalNumber(ec, nan), nil
}
var window int64
if len(windowStr) > 0 {
var err error
window, err = PositiveDurationValue(windowStr, ec.Step)
window, err = promql.DurationValue(windowStr, ec.Step)
if err != nil {
return nil, err
}
@ -595,7 +597,9 @@ func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, me
ProjectID: ec.AuthToken.ProjectID,
MinTimestamp: start - window - maxSilenceInterval,
MaxTimestamp: ec.End + ec.Step,
TagFilterss: [][]storage.TagFilter{me.TagFilters},
TagFilterss: [][]storage.TagFilter{
*(*[]storage.TagFilter)(unsafe.Pointer(&me.TagFilters)),
},
}
rss, isPartial, err := netstorage.ProcessSearchQuery(ec.AuthToken, sq, true, ec.Deadline)
if err != nil {

View File

@ -11,6 +11,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
"github.com/VictoriaMetrics/metrics"
)
@ -85,12 +86,12 @@ func Exec(ec *EvalConfig, q string, isFirstPointOnly bool) ([]netstorage.Result,
return result, err
}
func maySortResults(e expr, tss []*timeseries) bool {
func maySortResults(e promql.Expr, tss []*timeseries) bool {
if len(tss) > 100 {
// There is no sense in sorting a lot of results
return false
}
fe, ok := e.(*funcExpr)
fe, ok := e.(*promql.FuncExpr)
if !ok {
return true
}
@ -154,7 +155,16 @@ func removeNaNs(tss []*timeseries) []*timeseries {
return rvs
}
func parsePromQLWithCache(q string) (expr, error) {
func parsePromQL(q string) (promql.Expr, error) {
e, err := parser.ParsePromQL(q)
if err != nil {
return nil, err
}
return simplifyConstants(e), nil
}
func parsePromQLWithCache(q string) (promql.Expr, error) {
pcv := parseCacheV.Get(q)
if pcv == nil {
e, err := parsePromQL(q)
@ -170,6 +180,8 @@ func parsePromQLWithCache(q string) (expr, error) {
return pcv.e, nil
}
var parser = promql.NewParser(compileRegexpAnchored)
var parseCacheV = func() *parseCache {
pc := &parseCache{
m: make(map[string]*parseCacheValue),
@ -189,7 +201,7 @@ var parseCacheV = func() *parseCache {
const parseCacheMaxLen = 10e3
type parseCacheValue struct {
e expr
e promql.Expr
err error
}
@ -249,3 +261,7 @@ func (pc *parseCache) Put(q string, pcv *parseCacheValue) {
pc.m[q] = pcv
pc.mu.Unlock()
}
func init() {
promql.Panicf = logger.Panicf
}

View File

@ -0,0 +1,47 @@
package promql
import (
"fmt"
"unsafe"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
// IsMetricSelectorWithRollup verifies whether s contains PromQL metric selector
// wrapped into rollup.
//
// It returns the wrapped query with the corresponding window with offset.
func IsMetricSelectorWithRollup(s string) (childQuery string, window, offset string) {
expr, err := parsePromQLWithCache(s)
if err != nil {
return
}
re, ok := expr.(*promql.RollupExpr)
if !ok || len(re.Window) == 0 || len(re.Step) > 0 {
return
}
me, ok := re.Expr.(*promql.MetricExpr)
if !ok || len(me.TagFilters) == 0 {
return
}
wrappedQuery := me.AppendString(nil)
return string(wrappedQuery), re.Window, re.Offset
}
// ParseMetricSelector parses s containing PromQL metric selector
// and returns the corresponding TagFilters.
func ParseMetricSelector(s string) ([]storage.TagFilter, error) {
expr, err := parsePromQLWithCache(s)
if err != nil {
return nil, err
}
me, ok := expr.(*promql.MetricExpr)
if !ok {
return nil, fmt.Errorf("expecting metricSelector; got %q", expr.AppendString(nil))
}
if len(me.TagFilters) == 0 {
return nil, fmt.Errorf("tagFilters cannot be empty")
}
return *(*[]storage.TagFilter)(unsafe.Pointer(&me.TagFilters)), nil
}

View File

@ -0,0 +1,49 @@
package promql
import (
"testing"
)
func TestParseMetricSelectorSuccess(t *testing.T) {
f := func(s string) {
t.Helper()
tfs, err := ParseMetricSelector(s)
if err != nil {
t.Fatalf("unexpected error when parsing %q: %s", s, err)
}
if tfs == nil {
t.Fatalf("expecting non-nil tfs when parsing %q", s)
}
}
f("foo")
f(":foo")
f(" :fo:bar.baz")
f(`a{}`)
f(`{foo="bar"}`)
f(`{:f:oo=~"bar.+"}`)
f(`foo {bar != "baz"}`)
f(` foo { bar !~ "^ddd(x+)$", a="ss", __name__="sffd"} `)
f(`(foo)`)
}
func TestParseMetricSelectorError(t *testing.T) {
f := func(s string) {
t.Helper()
tfs, err := ParseMetricSelector(s)
if err == nil {
t.Fatalf("expecting non-nil error when parsing %q", s)
}
if tfs != nil {
t.Fatalf("expecting nil tfs when parsing %q", s)
}
}
f("")
f(`{}`)
f(`foo bar`)
f(`foo+bar`)
f(`sum(bar)`)
f(`x{y}`)
f(`x{y+z}`)
f(`foo[5m]`)
f(`foo offset 5m`)
}

File diff suppressed because it is too large Load Diff

View File

@ -103,10 +103,6 @@ func getRollupFunc(funcName string) newRollupFunc {
return rollupFuncs[funcName]
}
func isRollupFunc(funcName string) bool {
return getRollupFunc(funcName) != nil
}
type rollupFuncArg struct {
prevValue float64
prevTimestamp int64

View File

@ -13,6 +13,8 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
"github.com/VictoriaMetrics/fastcache"
"github.com/VictoriaMetrics/metrics"
@ -133,7 +135,7 @@ func ResetRollupResultCache() {
rollupResultCacheV.c.Reset()
}
func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExpr, iafc *incrementalAggrFuncContext, window int64) (tss []*timeseries, newStart int64) {
func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *promql.MetricExpr, iafc *incrementalAggrFuncContext, window int64) (tss []*timeseries, newStart int64) {
if *disableCache || !ec.mayCache() {
return nil, ec.Start
}
@ -214,7 +216,7 @@ func (rrc *rollupResultCache) Get(funcName string, ec *EvalConfig, me *metricExp
var resultBufPool bytesutil.ByteBufferPool
func (rrc *rollupResultCache) Put(funcName string, ec *EvalConfig, me *metricExpr, iafc *incrementalAggrFuncContext, window int64, tss []*timeseries) {
func (rrc *rollupResultCache) Put(funcName string, ec *EvalConfig, me *promql.MetricExpr, iafc *incrementalAggrFuncContext, window int64, tss []*timeseries) {
if *disableCache || len(tss) == 0 || !ec.mayCache() {
return
}
@ -295,7 +297,7 @@ var tooBigRollupResults = metrics.NewCounter("vm_too_big_rollup_results_total")
// Increment this value every time the format of the cache changes.
const rollupResultCacheVersion = 6
func marshalRollupResultCacheKey(dst []byte, funcName string, at *auth.Token, me *metricExpr, iafc *incrementalAggrFuncContext, window, step int64) []byte {
func marshalRollupResultCacheKey(dst []byte, funcName string, at *auth.Token, me *promql.MetricExpr, iafc *incrementalAggrFuncContext, window, step int64) []byte {
dst = append(dst, rollupResultCacheVersion)
if iafc == nil {
dst = append(dst, 0)
@ -310,7 +312,8 @@ func marshalRollupResultCacheKey(dst []byte, funcName string, at *auth.Token, me
dst = encoding.MarshalInt64(dst, window)
dst = encoding.MarshalInt64(dst, step)
for i := range me.TagFilters {
dst = me.TagFilters[i].Marshal(dst)
stf := storage.TagFilter(me.TagFilters[i])
dst = stf.Marshal(dst)
}
return dst
}

View File

@ -4,6 +4,7 @@ import (
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
@ -23,14 +24,14 @@ func TestRollupResultCache(t *testing.T) {
MayCache: true,
}
me := &metricExpr{
TagFilters: []storage.TagFilter{{
me := &promql.MetricExpr{
TagFilters: []promql.TagFilter{{
Key: []byte("aaa"),
Value: []byte("xxx"),
}},
}
iafc := &incrementalAggrFuncContext{
ae: &aggrFuncExpr{
ae: &promql.AggrFuncExpr{
Name: "foobar",
},
}

View File

@ -1,6 +1,7 @@
package promql
import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
"math"
"testing"
)
@ -157,7 +158,7 @@ func TestDerivValues(t *testing.T) {
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
}
func testRollupFunc(t *testing.T, funcName string, args []interface{}, meExpected *metricExpr, vExpected float64) {
func testRollupFunc(t *testing.T, funcName string, args []interface{}, meExpected *promql.MetricExpr, vExpected float64) {
t.Helper()
nrf := getRollupFunc(funcName)
if nrf == nil {
@ -197,8 +198,8 @@ func TestRollupQuantileOverTime(t *testing.T) {
Values: []float64{phi},
Timestamps: []int64{123},
}}
var me metricExpr
args := []interface{}{phis, &rollupExpr{Expr: &me}}
var me promql.MetricExpr
args := []interface{}{phis, &promql.RollupExpr{Expr: &me}}
testRollupFunc(t, "quantile_over_time", args, &me, vExpected)
}
@ -219,8 +220,8 @@ func TestRollupPredictLinear(t *testing.T) {
Values: []float64{sec},
Timestamps: []int64{123},
}}
var me metricExpr
args := []interface{}{&rollupExpr{Expr: &me}, secs}
var me promql.MetricExpr
args := []interface{}{&promql.RollupExpr{Expr: &me}, secs}
testRollupFunc(t, "predict_linear", args, &me, vExpected)
}
@ -241,8 +242,8 @@ func TestRollupHoltWinters(t *testing.T) {
Values: []float64{tf},
Timestamps: []int64{123},
}}
var me metricExpr
args := []interface{}{&rollupExpr{Expr: &me}, sfs, tfs}
var me promql.MetricExpr
args := []interface{}{&promql.RollupExpr{Expr: &me}, sfs, tfs}
testRollupFunc(t, "holt_winters", args, &me, vExpected)
}
@ -265,8 +266,8 @@ func TestRollupHoltWinters(t *testing.T) {
func TestRollupNewRollupFuncSuccess(t *testing.T) {
f := func(funcName string, vExpected float64) {
t.Helper()
var me metricExpr
args := []interface{}{&rollupExpr{Expr: &me}}
var me promql.MetricExpr
args := []interface{}{&promql.RollupExpr{Expr: &me}}
testRollupFunc(t, funcName, args, &me, vExpected)
}
@ -327,7 +328,7 @@ func TestRollupNewRollupFuncError(t *testing.T) {
Values: []float64{321},
Timestamps: []int64{123},
}}
me := &metricExpr{}
me := &promql.MetricExpr{}
f("holt_winters", []interface{}{123, 123, 321})
f("holt_winters", []interface{}{me, 123, 321})
f("holt_winters", []interface{}{me, scalarTs, 321})

View File

@ -0,0 +1,54 @@
package promql
import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
)
func simplifyConstants(e promql.Expr) promql.Expr {
if re, ok := e.(*promql.RollupExpr); ok {
re.Expr = simplifyConstants(re.Expr)
return re
}
if ae, ok := e.(*promql.AggrFuncExpr); ok {
simplifyConstantsInplace(ae.Args)
return ae
}
if fe, ok := e.(*promql.FuncExpr); ok {
simplifyConstantsInplace(fe.Args)
return fe
}
if pe, ok := e.(*promql.ParensExpr); ok {
if len(*pe) == 1 {
return simplifyConstants((*pe)[0])
}
simplifyConstantsInplace(*pe)
return pe
}
be, ok := e.(*promql.BinaryOpExpr)
if !ok {
return e
}
be.Left = simplifyConstants(be.Left)
be.Right = simplifyConstants(be.Right)
lne, ok := be.Left.(*promql.NumberExpr)
if !ok {
return be
}
rne, ok := be.Right.(*promql.NumberExpr)
if !ok {
return be
}
n := binaryOpConstants(be.Op, lne.N, rne.N, be.Bool)
ne := &promql.NumberExpr{
N: n,
}
return ne
}
func simplifyConstantsInplace(args []promql.Expr) {
for i, arg := range args {
args[i] = simplifyConstants(arg)
}
}

View File

@ -0,0 +1,89 @@
package promql
import (
"testing"
)
func TestSimplify(t *testing.T) {
another := func(s string, sExpected string) {
t.Helper()
e, err := parsePromQL(s)
if err != nil {
t.Fatalf("unexpected error when parsing %q: %s", s, err)
}
res := e.AppendString(nil)
if string(res) != sExpected {
t.Fatalf("unexpected string constructed;\ngot\n%q\nwant\n%q", res, sExpected)
}
}
// Simplification cases
another(`nan == nan`, `NaN`)
another(`nan ==bool nan`, `1`)
another(`nan !=bool nan`, `0`)
another(`nan !=bool 2`, `1`)
another(`2 !=bool nan`, `1`)
another(`nan >bool nan`, `0`)
another(`nan <bool nan`, `0`)
another(`1 ==bool nan`, `0`)
another(`NaN !=bool 1`, `1`)
another(`inf >=bool 2`, `1`)
another(`-1 >bool -inf`, `1`)
another(`-1 <bool -inf`, `0`)
another(`nan + 2 *3 * inf`, `NaN`)
another(`INF - Inf`, `NaN`)
another(`Inf + inf`, `+Inf`)
another(`1/0`, `+Inf`)
another(`0/0`, `NaN`)
another(`1 or 2`, `1`)
another(`1 and 2`, `1`)
another(`1 unless 2`, `NaN`)
another(`1 default 2`, `1`)
another(`1 default NaN`, `1`)
another(`NaN default 2`, `2`)
another(`1 > 2`, `NaN`)
another(`1 > bool 2`, `0`)
another(`3 >= 2`, `3`)
another(`3 <= bool 2`, `0`)
another(`1 + -2 - 3`, `-4`)
another(`1 / 0 + 2`, `+Inf`)
another(`2 + -1 / 0`, `-Inf`)
another(`-1 ^ 0.5`, `NaN`)
another(`512.5 - (1 + 3) * (2 ^ 2) ^ 3`, `256.5`)
another(`1 == bool 1 != bool 24 < bool 4 > bool -1`, `1`)
another(`1 == bOOl 1 != BOOL 24 < Bool 4 > booL -1`, `1`)
another(`1+2 if 2>3`, `NaN`)
another(`1+4 if 2<3`, `5`)
another(`2+6 default 3 if 2>3`, `8`)
another(`2+6 if 2>3 default NaN`, `NaN`)
another(`42 if 3>2 if 2+2<5`, `42`)
another(`42 if 3>2 if 2+2>=5`, `NaN`)
another(`1+2 ifnot 2>3`, `3`)
another(`1+4 ifnot 2<3`, `NaN`)
another(`2+6 default 3 ifnot 2>3`, `8`)
another(`2+6 ifnot 2>3 default NaN`, `8`)
another(`42 if 3>2 ifnot 2+2<5`, `NaN`)
another(`42 if 3>2 ifnot 2+2>=5`, `42`)
another(`5 - 1 + 3 * 2 ^ 2 ^ 3 - 2 OR Metric {Bar= "Baz", aaa!="bb",cc=~"dd" ,zz !~"ff" } `,
`770 or Metric{Bar="Baz", aaa!="bb", cc=~"dd", zz!~"ff"}`)
another(`((foo(bar,baz)), (1+(2)+(3,4)+()))`, `(foo(bar, baz), (3 + (3, 4)) + ())`)
another(` FOO (bar) + f ( m ( ),ff(1 + ( 2.5)) ,M[5m ] , "ff" )`, `FOO(bar) + f(m(), ff(3.5), M[5m], "ff")`)
another(`sum without (a, b) (xx,2+2)`, `sum(xx, 4) without (a, b)`)
another(`Sum WIthout (a, B) (XX,2+2)`, `sum(XX, 4) without (a, B)`)
another(`with (foo = bar{x="x"}) 1+1`, `2`)
another(`with (x(a, b) = a + b) x(foo, x(1, 2))`, `foo + 3`)
another(`WITH (
x2(x) = x^2,
f(x, y) = x2(x) + x*y + x2(y)
)
f(a, 3)
`, `((a ^ 2) + (a * 3)) + 9`)
another(`WITH (
x2(x) = x^2,
f(x, y) = x2(x) + x*y + x2(y)
)
f(2, 3)
`, `19`)
another(`with(y=123,z=5) union(with(y=3,f(x)=x*y) f(2) + f(3), with(x=5,y=2) x*y*z)`, `union(15, 50)`)
}

View File

@ -12,6 +12,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/valyala/histogram"
)
@ -99,13 +100,9 @@ func getTransformFunc(s string) transformFunc {
return transformFuncs[s]
}
func isTransformFunc(s string) bool {
return getTransformFunc(s) != nil
}
type transformFuncArg struct {
ec *EvalConfig
fe *funcExpr
fe *promql.FuncExpr
args [][]*timeseries
}
@ -126,7 +123,7 @@ func newTransformFuncOneArg(tf func(v float64) float64) transformFunc {
}
}
func doTransformValues(arg []*timeseries, tf func(values []float64), fe *funcExpr) ([]*timeseries, error) {
func doTransformValues(arg []*timeseries, tf func(values []float64), fe *promql.FuncExpr) ([]*timeseries, error) {
name := strings.ToLower(fe.Name)
keepMetricGroup := transformFuncsKeepMetricGroup[name]
for _, ts := range arg {
@ -154,7 +151,7 @@ func transformAbsent(tfa *transformFuncArg) ([]*timeseries, error) {
// Copy tags from arg
rvs := evalNumber(tfa.ec, 1)
rv := rvs[0]
me, ok := tfa.fe.Args[0].(*metricExpr)
me, ok := tfa.fe.Args[0].(*promql.MetricExpr)
if !ok {
return rvs, nil
}
@ -1163,7 +1160,7 @@ func transformScalar(tfa *transformFuncArg) ([]*timeseries, error) {
// Verify whether the arg is a string.
// Then try converting the string to number.
if se, ok := tfa.fe.Args[0].(*stringExpr); ok {
if se, ok := tfa.fe.Args[0].(*promql.StringExpr); ok {
n, err := strconv.ParseFloat(se.S, 64)
if err != nil {
n = nan

51
lib/promql/aggr.go Normal file
View File

@ -0,0 +1,51 @@
package promql
import (
"strings"
)
var aggrFuncs = map[string]bool{
// See https://prometheus.io/docs/prometheus/latest/querying/operators/#aggregation-operators
"sum": true,
"min": true,
"max": true,
"avg": true,
"stddev": true,
"stdvar": true,
"count": true,
"count_values": true,
"bottomk": true,
"topk": true,
"quantile": true,
// Extended PromQL funcs
"median": true,
"limitk": true,
"distinct": true,
"sum2": true,
"geomean": true,
"histogram": true,
"topk_min": true,
"topk_max": true,
"topk_avg": true,
"topk_median": true,
"bottomk_min": true,
"bottomk_max": true,
"bottomk_avg": true,
"bottomk_median": true,
}
func isAggrFunc(s string) bool {
s = strings.ToLower(s)
return aggrFuncs[s]
}
func isAggrFuncModifier(s string) bool {
s = strings.ToLower(s)
switch s {
case "by", "without":
return true
default:
return false
}
}

109
lib/promql/binary_op.go Normal file
View File

@ -0,0 +1,109 @@
package promql
import (
"strings"
)
var binaryOpPriorities = map[string]int{
"default": -1,
"if": 0,
"ifnot": 0,
// See https://prometheus.io/docs/prometheus/latest/querying/operators/#binary-operator-precedence
"or": 1,
"and": 2,
"unless": 2,
"==": 3,
"!=": 3,
"<": 3,
">": 3,
"<=": 3,
">=": 3,
"+": 4,
"-": 4,
"*": 5,
"/": 5,
"%": 5,
"^": 6,
}
func isBinaryOp(op string) bool {
op = strings.ToLower(op)
_, ok := binaryOpPriorities[op]
return ok
}
func binaryOpPriority(op string) int {
op = strings.ToLower(op)
return binaryOpPriorities[op]
}
func scanBinaryOpPrefix(s string) int {
n := 0
for op := range binaryOpPriorities {
if len(s) < len(op) {
continue
}
ss := strings.ToLower(s[:len(op)])
if ss == op && len(op) > n {
n = len(op)
}
}
return n
}
func isRightAssociativeBinaryOp(op string) bool {
// See https://prometheus.io/docs/prometheus/latest/querying/operators/#binary-operator-precedence
return op == "^"
}
func isBinaryOpGroupModifier(s string) bool {
s = strings.ToLower(s)
switch s {
// See https://prometheus.io/docs/prometheus/latest/querying/operators/#vector-matching
case "on", "ignoring":
return true
default:
return false
}
}
func isBinaryOpJoinModifier(s string) bool {
s = strings.ToLower(s)
switch s {
case "group_left", "group_right":
return true
default:
return false
}
}
func isBinaryOpBoolModifier(s string) bool {
s = strings.ToLower(s)
return s == "bool"
}
func isBinaryOpCmp(op string) bool {
switch op {
case "==", "!=", ">", "<", ">=", "<=":
return true
default:
return false
}
}
func isBinaryOpLogicalSet(op string) bool {
op = strings.ToLower(op)
switch op {
case "and", "or", "unless":
return true
default:
return false
}
}

423
lib/promql/expr.go Normal file
View File

@ -0,0 +1,423 @@
package promql
import (
"fmt"
"strconv"
)
// An Expr represents a parsed Extended PromQL expression
type Expr interface {
// AppendString appends string representation of expr to dst.
AppendString(dst []byte) []byte
}
// A StringExpr represents a string
type StringExpr struct {
S string
}
// AppendString appends string representation of expr to dst.
func (se *StringExpr) AppendString(dst []byte) []byte {
return strconv.AppendQuote(dst, se.S)
}
// A StringTemplateExpr represents a string prior to applying a With clause
type StringTemplateExpr struct {
// Composite string has non-empty tokens.
Tokens []StringToken
}
// AppendString appends string representation of expr to dst.
func (ste *StringTemplateExpr) AppendString(dst []byte) []byte {
if ste == nil {
return dst
}
for i, tok := range ste.Tokens {
if i > 0 {
dst = append(dst, " + "...)
}
dst = tok.AppendString(dst)
}
return dst
}
// A StringToken represents a portion of a string expression
type StringToken struct {
Ident bool
S string
}
// AppendString appends string representation of st to dst.
func (st *StringToken) AppendString(dst []byte) []byte {
if st.Ident {
return appendEscapedIdent(dst, []byte(st.S))
}
return strconv.AppendQuote(dst, st.S)
}
// A NumberExpr represents a number
type NumberExpr struct {
N float64
}
// AppendString appends string representation of expr to dst.
func (ne *NumberExpr) AppendString(dst []byte) []byte {
return strconv.AppendFloat(dst, ne.N, 'g', -1, 64)
}
// A ParensExpr represents a parens expression
type ParensExpr []Expr
// AppendString appends string representation of expr to dst.
func (pe ParensExpr) AppendString(dst []byte) []byte {
return appendStringArgListExpr(dst, pe)
}
// A BinaryOpExpr represents a binary operator
type BinaryOpExpr struct {
Op string
Bool bool
GroupModifier ModifierExpr
JoinModifier ModifierExpr
Left Expr
Right Expr
}
// AppendString appends string representation of expr to dst.
func (be *BinaryOpExpr) AppendString(dst []byte) []byte {
if _, ok := be.Left.(*BinaryOpExpr); ok {
dst = append(dst, '(')
dst = be.Left.AppendString(dst)
dst = append(dst, ')')
} else {
dst = be.Left.AppendString(dst)
}
dst = append(dst, ' ')
dst = append(dst, be.Op...)
if be.Bool {
dst = append(dst, " bool"...)
}
if be.GroupModifier.Op != "" {
dst = append(dst, ' ')
dst = be.GroupModifier.AppendString(dst)
}
if be.JoinModifier.Op != "" {
dst = append(dst, ' ')
dst = be.JoinModifier.AppendString(dst)
}
dst = append(dst, ' ')
if _, ok := be.Right.(*BinaryOpExpr); ok {
dst = append(dst, '(')
dst = be.Right.AppendString(dst)
dst = append(dst, ')')
} else {
dst = be.Right.AppendString(dst)
}
return dst
}
// A ModifierExpr represents a modifier attached to a parent expression
type ModifierExpr struct {
Op string
Args []string
}
// AppendString appends string representation of expr to dst.
func (me *ModifierExpr) AppendString(dst []byte) []byte {
dst = append(dst, me.Op...)
dst = append(dst, " ("...)
for i, arg := range me.Args {
dst = append(dst, arg...)
if i+1 < len(me.Args) {
dst = append(dst, ", "...)
}
}
dst = append(dst, ')')
return dst
}
func appendStringArgListExpr(dst []byte, args []Expr) []byte {
dst = append(dst, '(')
for i, arg := range args {
dst = arg.AppendString(dst)
if i+1 < len(args) {
dst = append(dst, ", "...)
}
}
dst = append(dst, ')')
return dst
}
// A FuncExpr represents a function invocation
type FuncExpr struct {
Name string
Args []Expr
}
// AppendString appends string representation of expr to dst.
func (fe *FuncExpr) AppendString(dst []byte) []byte {
dst = append(dst, fe.Name...)
dst = appendStringArgListExpr(dst, fe.Args)
return dst
}
// An AggrFuncExpr represents the invocation of an aggregate function
type AggrFuncExpr struct {
Name string
Args []Expr
Modifier ModifierExpr
}
// AppendString appends string representation of expr to dst.
func (ae *AggrFuncExpr) AppendString(dst []byte) []byte {
dst = append(dst, ae.Name...)
dst = appendStringArgListExpr(dst, ae.Args)
if ae.Modifier.Op != "" {
dst = append(dst, ' ')
dst = ae.Modifier.AppendString(dst)
}
return dst
}
// A WithExpr represents a With expression
type WithExpr struct {
Was []*WithArgExpr
Expr Expr
}
// AppendString appends string representation of expr to dst.
func (we *WithExpr) AppendString(dst []byte) []byte {
dst = append(dst, "WITH ("...)
for i, wa := range we.Was {
dst = wa.AppendString(dst)
if i+1 < len(we.Was) {
dst = append(dst, ',')
}
}
dst = append(dst, ") "...)
dst = we.Expr.AppendString(dst)
return dst
}
// A WithArgExpr represents an arg in a With expression
type WithArgExpr struct {
Name string
Args []string
Expr Expr
}
// AppendString appends string representation of expr to dst.
func (wa *WithArgExpr) AppendString(dst []byte) []byte {
dst = append(dst, wa.Name...)
if len(wa.Args) > 0 {
dst = append(dst, '(')
for i, arg := range wa.Args {
dst = append(dst, arg...)
if i+1 < len(wa.Args) {
dst = append(dst, ',')
}
}
dst = append(dst, ')')
}
dst = append(dst, " = "...)
dst = wa.Expr.AppendString(dst)
return dst
}
// A RollupExpr represents a rollup expression
type RollupExpr struct {
// The expression for the rollup. Usually it is metricExpr, but may be arbitrary expr
// if subquery is used. https://prometheus.io/blog/2019/01/28/subquery-support/
Expr Expr
// Window contains optional window value from square brackets
//
// For example, `http_requests_total[5m]` will have Window value `5m`.
Window string
// Offset contains optional value from `offset` part.
//
// For example, `foobar{baz="aa"} offset 5m` will have Offset value `5m`.
Offset string
// Step contains optional step value from square brackets.
//
// For example, `foobar[1h:3m]` will have Step value '3m'.
Step string
// If set to true, then `foo[1h:]` would print the same
// instead of `foo[1h]`.
InheritStep bool
}
// ForSubquery returns whether is rollup is for a subquery
func (re *RollupExpr) ForSubquery() bool {
return len(re.Step) > 0 || re.InheritStep
}
// AppendString appends string representation of expr to dst.
func (re *RollupExpr) AppendString(dst []byte) []byte {
needParens := func() bool {
if _, ok := re.Expr.(*RollupExpr); ok {
return true
}
if _, ok := re.Expr.(*BinaryOpExpr); ok {
return true
}
if ae, ok := re.Expr.(*AggrFuncExpr); ok && ae.Modifier.Op != "" {
return true
}
return false
}()
if needParens {
dst = append(dst, '(')
}
dst = re.Expr.AppendString(dst)
if needParens {
dst = append(dst, ')')
}
if len(re.Window) > 0 || re.InheritStep || len(re.Step) > 0 {
dst = append(dst, '[')
if len(re.Window) > 0 {
dst = append(dst, re.Window...)
}
if len(re.Step) > 0 {
dst = append(dst, ':')
dst = append(dst, re.Step...)
} else if re.InheritStep {
dst = append(dst, ':')
}
dst = append(dst, ']')
}
if len(re.Offset) > 0 {
dst = append(dst, " offset "...)
dst = append(dst, re.Offset...)
}
return dst
}
// A MetricExpr represents a metric expression
type MetricExpr struct {
// TagFilters contains a list of tag filters from curly braces.
// The first item may be the metric name.
TagFilters []TagFilter
}
// AppendString appends string representation of expr to dst.
func (me *MetricExpr) AppendString(dst []byte) []byte {
tfs := me.TagFilters
if len(tfs) > 0 {
tf := &tfs[0]
if len(tf.Key) == 0 && !tf.IsNegative && !tf.IsRegexp {
dst = appendEscapedIdent(dst, tf.Value)
tfs = tfs[1:]
}
}
if len(tfs) > 0 {
dst = append(dst, '{')
for i := range tfs {
tf := &tfs[i]
dst = appendStringTagFilter(dst, tf)
if i+1 < len(tfs) {
dst = append(dst, ", "...)
}
}
dst = append(dst, '}')
} else if len(me.TagFilters) == 0 {
dst = append(dst, "{}"...)
}
return dst
}
// IsEmpty returns whether this is an empty metric expression
func (me *MetricExpr) IsEmpty() bool {
return len(me.TagFilters) == 0
}
// IsOnlyMetricGroup returns whether this is a metric group only
func (me *MetricExpr) IsOnlyMetricGroup() bool {
if !me.HasNonEmptyMetricGroup() {
return false
}
return len(me.TagFilters) == 1
}
// HasNonEmptyMetricGroup returns whether this has a non-empty metric group
func (me *MetricExpr) HasNonEmptyMetricGroup() bool {
if len(me.TagFilters) == 0 {
return false
}
tf := &me.TagFilters[0]
return len(tf.Key) == 0 && !tf.IsNegative && !tf.IsRegexp
}
// A TagFilter is a single key <op> value filter tag in a metric filter
//
// Note that this should exactly match the definition in the stroage package
type TagFilter struct {
Key []byte
Value []byte
IsNegative bool
IsRegexp bool
}
// A MetricTemplateExpr represents a metric expression prior to expansion via
// a with clause
type MetricTemplateExpr struct {
TagFilters []*TagFilterExpr
}
// AppendString appends string representation of expr to dst.
func (mte *MetricTemplateExpr) AppendString(dst []byte) []byte {
tfs := mte.TagFilters
if len(tfs) > 0 {
tf := tfs[0]
if len(tf.Key) == 0 && !tf.IsNegative && !tf.IsRegexp && len(tf.Value.Tokens) == 1 && !tf.Value.Tokens[0].Ident {
dst = appendEscapedIdent(dst, []byte(tf.Value.Tokens[0].S))
tfs = tfs[1:]
}
}
if len(tfs) > 0 {
dst = append(dst, '{')
for i := range tfs {
tf := tfs[i]
dst = tf.AppendString(dst)
if i+1 < len(tfs) {
dst = append(dst, ", "...)
}
}
dst = append(dst, '}')
} else if len(mte.TagFilters) == 0 {
dst = append(dst, "{}"...)
}
return dst
}
// A TagFilterExpr represents a tag filter
type TagFilterExpr struct {
Key string
Value *StringTemplateExpr
IsRegexp bool
IsNegative bool
}
func (tfe *TagFilterExpr) String() string {
return fmt.Sprintf("[key=%q, value=%+v, isRegexp=%v, isNegative=%v]", tfe.Key, tfe.Value, tfe.IsRegexp, tfe.IsNegative)
}
// AppendString appends string representation of expr to dst.
func (tfe *TagFilterExpr) AppendString(dst []byte) []byte {
if len(tfe.Key) == 0 {
dst = append(dst, "__name__"...)
} else {
dst = append(dst, tfe.Key...)
}
dst = appendStringTagFilterOp(dst, tfe.IsRegexp, tfe.IsNegative)
return tfe.Value.AppendString(dst)
}

View File

@ -4,8 +4,6 @@ import (
"fmt"
"strconv"
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
type lexer struct {
@ -222,7 +220,7 @@ func scanIdent(s string) string {
}
}
if i == 0 {
logger.Panicf("BUG: scanIdent couldn't find a single ident char; make sure isIdentPrefix called before scanIdent")
Panicf("BUG: scanIdent couldn't find a single ident char; make sure isIdentPrefix called before scanIdent")
}
return s[:i]
}

1298
lib/promql/parser.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,58 +1,24 @@
package promql
import (
"regexp"
"testing"
)
func TestParseMetricSelectorSuccess(t *testing.T) {
f := func(s string) {
t.Helper()
tfs, err := ParseMetricSelector(s)
if err != nil {
t.Fatalf("unexpected error when parsing %q: %s", s, err)
}
if tfs == nil {
t.Fatalf("expecting non-nil tfs when parsing %q", s)
}
}
f("foo")
f(":foo")
f(" :fo:bar.baz")
f(`a{}`)
f(`{foo="bar"}`)
f(`{:f:oo=~"bar.+"}`)
f(`foo {bar != "baz"}`)
f(` foo { bar !~ "^ddd(x+)$", a="ss", __name__="sffd"} `)
f(`(foo)`)
var testParser = &Parser{
compileRegexpAnchored: compileRegexpAnchored,
}
func TestParseMetricSelectorError(t *testing.T) {
f := func(s string) {
t.Helper()
tfs, err := ParseMetricSelector(s)
if err == nil {
t.Fatalf("expecting non-nil error when parsing %q", s)
}
if tfs != nil {
t.Fatalf("expecting nil tfs when parsing %q", s)
}
}
f("")
f(`{}`)
f(`foo bar`)
f(`foo+bar`)
f(`sum(bar)`)
f(`x{y}`)
f(`x{y+z}`)
f(`foo[5m]`)
f(`foo offset 5m`)
func compileRegexpAnchored(re string) (*regexp.Regexp, error) {
reAnchored := "^(?:" + re + ")$"
return regexp.Compile(reAnchored)
}
func TestParsePromQLSuccess(t *testing.T) {
another := func(s string, sExpected string) {
t.Helper()
e, err := parsePromQL(s)
e, err := testParser.ParsePromQL(s)
if err != nil {
t.Fatalf("unexpected error when parsing %q: %s", s, err)
}
@ -184,72 +150,24 @@ func TestParsePromQLSuccess(t *testing.T) {
another(`-inF`, `-Inf`)
// binaryOpExpr
another(`nan == nan`, `NaN`)
another(`nan ==bool nan`, `1`)
another(`nan !=bool nan`, `0`)
another(`nan !=bool 2`, `1`)
another(`2 !=bool nan`, `1`)
another(`nan >bool nan`, `0`)
another(`nan <bool nan`, `0`)
another(`1 ==bool nan`, `0`)
another(`NaN !=bool 1`, `1`)
another(`inf >=bool 2`, `1`)
another(`-1 >bool -inf`, `1`)
another(`-1 <bool -inf`, `0`)
another(`nan + 2 *3 * inf`, `NaN`)
another(`INF - Inf`, `NaN`)
another(`Inf + inf`, `+Inf`)
another(`1/0`, `+Inf`)
another(`0/0`, `NaN`)
another(`-m`, `0 - m`)
same(`m + ignoring () n[5m]`)
another(`M + IGNORING () N[5m]`, `M + ignoring () N[5m]`)
same(`m + on (foo) n[5m]`)
another(`m + ON (Foo) n[5m]`, `m + on (Foo) n[5m]`)
same(`m + ignoring (a, b) n[5m]`)
another(`1 or 2`, `1`)
another(`1 and 2`, `1`)
another(`1 unless 2`, `NaN`)
another(`1 default 2`, `1`)
another(`1 default NaN`, `1`)
another(`NaN default 2`, `2`)
another(`1 > 2`, `NaN`)
another(`1 > bool 2`, `0`)
another(`3 >= 2`, `3`)
another(`3 <= bool 2`, `0`)
another(`1 + -2 - 3`, `-4`)
another(`1 / 0 + 2`, `+Inf`)
another(`2 + -1 / 0`, `-Inf`)
another(`-1 ^ 0.5`, `NaN`)
another(`512.5 - (1 + 3) * (2 ^ 2) ^ 3`, `256.5`)
another(`1 == bool 1 != bool 24 < bool 4 > bool -1`, `1`)
another(`1 == bOOl 1 != BOOL 24 < Bool 4 > booL -1`, `1`)
another(`m1+on(foo)group_left m2`, `m1 + on (foo) group_left () m2`)
another(`M1+ON(FOO)GROUP_left M2`, `M1 + on (FOO) group_left () M2`)
same(`m1 + on (foo) group_right () m2`)
same(`m1 + on (foo, bar) group_right (x, y) m2`)
another(`m1 + on (foo, bar,) group_right (x, y,) m2`, `m1 + on (foo, bar) group_right (x, y) m2`)
same(`m1 == bool on (foo, bar) group_right (x, y) m2`)
another(`5 - 1 + 3 * 2 ^ 2 ^ 3 - 2 OR Metric {Bar= "Baz", aaa!="bb",cc=~"dd" ,zz !~"ff" } `,
`770 or Metric{Bar="Baz", aaa!="bb", cc=~"dd", zz!~"ff"}`)
same(`"foo" + bar()`)
same(`"foo" + bar{x="y"}`)
same(`("foo"[3s] + bar{x="y"})[5m:3s] offset 10s`)
same(`("foo"[3s] + bar{x="y"})[5i:3i] offset 10i`)
same(`bar + "foo" offset 3s`)
same(`bar + "foo" offset 3i`)
another(`1+2 if 2>3`, `NaN`)
another(`1+4 if 2<3`, `5`)
another(`2+6 default 3 if 2>3`, `8`)
another(`2+6 if 2>3 default NaN`, `NaN`)
another(`42 if 3>2 if 2+2<5`, `42`)
another(`42 if 3>2 if 2+2>=5`, `NaN`)
another(`1+2 ifnot 2>3`, `3`)
another(`1+4 ifnot 2<3`, `NaN`)
another(`2+6 default 3 ifnot 2>3`, `8`)
another(`2+6 ifnot 2>3 default NaN`, `8`)
another(`42 if 3>2 ifnot 2+2<5`, `NaN`)
another(`42 if 3>2 ifnot 2+2>=5`, `42`)
// parensExpr
another(`(-foo + ((bar) / (baz))) + ((23))`, `((0 - foo) + (bar / baz)) + 23`)
@ -258,7 +176,6 @@ func TestParsePromQLSuccess(t *testing.T) {
another(`((foo, bar),(baz))`, `((foo, bar), baz)`)
same(`(foo, (bar, baz), ((x, y), (z, y), xx))`)
another(`1+(foo, bar,)`, `1 + (foo, bar)`)
another(`((foo(bar,baz)), (1+(2)+(3,4)+()))`, `(foo(bar, baz), (3 + (3, 4)) + ())`)
same(`()`)
// funcExpr
@ -275,7 +192,7 @@ func TestParsePromQLSuccess(t *testing.T) {
same(`F(HttpServerRequest)`)
same(`f(job, foo)`)
same(`F(Job, Foo)`)
another(` FOO (bar) + f ( m ( ),ff(1 + ( 2.5)) ,M[5m ] , "ff" )`, `FOO(bar) + f(m(), ff(3.5), M[5m], "ff")`)
// funcName matching keywords
same(`by(2)`)
same(`BY(2)`)
@ -298,8 +215,6 @@ func TestParsePromQLSuccess(t *testing.T) {
another(`sum by () (xx)`, `sum(xx) by ()`)
another(`sum by (s) (xx)[5s]`, `(sum(xx) by (s))[5s]`)
another(`SUM BY (ZZ, aa) (XX)`, `sum(XX) by (ZZ, aa)`)
another(`sum without (a, b) (xx,2+2)`, `sum(xx, 4) without (a, b)`)
another(`Sum WIthout (a, B) (XX,2+2)`, `sum(XX, 4) without (a, B)`)
same(`sum(a) or sum(b)`)
same(`sum(a) by () or sum(b) without (x, y)`)
same(`sum(a) + sum(b)`)
@ -322,7 +237,7 @@ func TestParsePromQLSuccess(t *testing.T) {
another(`with (foo = bar{x="x"}) "x"`, `"x"`)
another(`with (f="x") f`, `"x"`)
another(`with (foo = bar{x="x"}) x{x="y"}`, `x{x="y"}`)
another(`with (foo = bar{x="x"}) 1+1`, `2`)
another(`with (foo = bar{x="x"}) 2`, `2`)
another(`with (foo = bar{x="x"}) f()`, `f()`)
another(`with (foo = bar{x="x"}) sum(x)`, `sum(x)`)
another(`with (foo = bar{x="x"}) baz{foo="bar"}`, `baz{foo="bar"}`)
@ -355,7 +270,6 @@ func TestParsePromQLSuccess(t *testing.T) {
another(`with (x() = y+1) x`, `y + 1`)
another(`with (x(foo) = foo+1) x(a)`, `a + 1`)
another(`with (x(a, b) = a + b) x(foo, bar)`, `foo + bar`)
another(`with (x(a, b) = a + b) x(foo, x(1, 2))`, `foo + 3`)
another(`with (x(a) = sum(a) by (b)) x(xx) / x(y)`, `sum(xx) by (b) / sum(y) by (b)`)
another(`with (f(a,f,x)=ff(x,f,a)) f(f(x,y,z),1,2)`, `ff(2, 1, ff(z, y, x))`)
another(`with (f(x)=1+f(x)) f(foo{bar="baz"})`, `1 + f(foo{bar="baz"})`)
@ -414,18 +328,6 @@ func TestParsePromQLSuccess(t *testing.T) {
)
hitRatio < treshold`,
`(sum(rate(cache{type="hit", job="cacher", instance=~"1.2.3.4"}[5m])) by (instance) / sum(rate(cache{type="hit", job="cacher", instance=~"1.2.3.4"}[5m]) + rate(cache{type="miss", job="cacher", instance=~"1.2.3.4"}[5m])) by (instance)) < 0.9`)
another(`WITH (
x2(x) = x^2,
f(x, y) = x2(x) + x*y + x2(y)
)
f(a, 3)
`, `((a ^ 2) + (a * 3)) + 9`)
another(`WITH (
x2(x) = x^2,
f(x, y) = x2(x) + x*y + x2(y)
)
f(2, 3)
`, `19`)
another(`WITH (
commonFilters = {instance="foo"},
timeToFuckup(currv, maxv) = (maxv - currv) / rate(currv)
@ -439,16 +341,15 @@ func TestParsePromQLSuccess(t *testing.T) {
)
hitRate(cacheHits, cacheMisses)`,
`sum(rate(cacheHits{job="foo", instance="bar"})) by (job, instance) / (sum(rate(cacheHits{job="foo", instance="bar"})) by (job, instance) + sum(rate(cacheMisses{job="foo", instance="bar"})) by (job, instance))`)
another(`with(y=123,z=5) union(with(y=3,f(x)=x*y) f(2) + f(3), with(x=5,y=2) x*y*z)`, `union(15, 50)`)
}
func TestParsePromQLError(t *testing.T) {
f := func(s string) {
t.Helper()
e, err := parsePromQL(s)
e, err := testParser.ParsePromQL(s)
if err == nil {
t.Fatalf("expecting non-nil error when parsing %q", s)
t.Fatalf("expecting non-nil error when parsing %q (expr=%v)", s, e)
}
if e != nil {
t.Fatalf("expecting nil expr when parsing %q", s)

56
lib/promql/rollup.go Normal file
View File

@ -0,0 +1,56 @@
package promql
import (
"strings"
)
var rollupFuncs = map[string]bool{
"default_rollup": true, // default rollup func
// Standard rollup funcs from PromQL.
// See funcs accepting range-vector on https://prometheus.io/docs/prometheus/latest/querying/functions/ .
"changes": true,
"delta": true,
"deriv": true,
"deriv_fast": true,
"holt_winters": true,
"idelta": true,
"increase": true,
"irate": true,
"predict_linear": true,
"rate": true,
"resets": true,
"avg_over_time": true,
"min_over_time": true,
"max_over_time": true,
"sum_over_time": true,
"count_over_time": true,
"quantile_over_time": true,
"stddev_over_time": true,
"stdvar_over_time": true,
// Additional rollup funcs.
"sum2_over_time": true,
"geomean_over_time": true,
"first_over_time": true,
"last_over_time": true,
"distinct_over_time": true,
"increases_over_time": true,
"decreases_over_time": true,
"integrate": true,
"ideriv": true,
"lifetime": true,
"lag": true,
"scrape_interval": true,
"rollup": true,
"rollup_rate": true,
"rollup_deriv": true,
"rollup_delta": true,
"rollup_increase": true,
"rollup_candlestick": true,
}
func isRollupFunc(funcName string) bool {
funcName = strings.ToLower(funcName)
return rollupFuncs[funcName]
}

80
lib/promql/transform.go Normal file
View File

@ -0,0 +1,80 @@
package promql
import (
"strings"
)
var transformFuncs = map[string]bool{
// Standard promql funcs
// See funcs accepting instant-vector on https://prometheus.io/docs/prometheus/latest/querying/functions/ .
"abs": true,
"absent": true,
"ceil": true,
"clamp_max": true,
"clamp_min": true,
"day_of_month": true,
"day_of_week": true,
"days_in_month": true,
"exp": true,
"floor": true,
"histogram_quantile": true,
"hour": true,
"label_join": true,
"label_replace": true,
"ln": true,
"log2": true,
"log10": true,
"minute": true,
"month": true,
"round": true,
"scalar": true,
"sort": true,
"sort_desc": true,
"sqrt": true,
"time": true,
"timestamp": true,
"vector": true,
"year": true,
// New funcs
"label_set": true,
"label_del": true,
"label_keep": true,
"label_copy": true,
"label_move": true,
"label_transform": true,
"label_value": true,
"union": true,
"": true,
"keep_last_value": true,
"start": true,
"end": true,
"step": true,
"running_sum": true,
"running_max": true,
"running_min": true,
"running_avg": true,
"range_sum": true,
"range_max": true,
"range_min": true,
"range_avg": true,
"range_first": true,
"range_last": true,
"range_quantile": true,
"smooth_exponential": true,
"remove_resets": true,
"rand": true,
"rand_normal": true,
"rand_exponential": true,
"pi": true,
"sin": true,
"cos": true,
"asin": true,
"acos": true,
"prometheus_buckets": true,
}
func isTransformFunc(s string) bool {
s = strings.ToLower(s)
return transformFuncs[s]
}

47
lib/promql/visitor.go Normal file
View File

@ -0,0 +1,47 @@
package promql
// A Visitor is used to walk a parsed query
type Visitor interface {
Visit(expr Expr) Visitor
}
// Walk invokes Visit on v for each node in the parsed query tree
func Walk(expr Expr, v Visitor) {
nv := v.Visit(expr)
if nv == nil {
return
}
switch t := expr.(type) {
case *ParensExpr:
for _, e := range *t {
Walk(e, nv)
}
case *BinaryOpExpr:
Walk(t.Left, nv)
Walk(t.Right, nv)
case *FuncExpr:
for _, ae := range t.Args {
Walk(ae, nv)
}
case *AggrFuncExpr:
for _, ae := range t.Args {
Walk(ae, nv)
}
Walk(&t.Modifier, nv)
case *WithExpr:
for _, wa := range t.Was {
Walk(wa, nv)
}
Walk(t.Expr, nv)
case *WithArgExpr:
Walk(t.Expr, nv)
case *RollupExpr:
Walk(t.Expr, nv)
case *MetricTemplateExpr:
for _, tfe := range t.TagFilters {
Walk(tfe, nv)
}
case *TagFilterExpr:
Walk(t.Value, nv)
}
}

View File

@ -0,0 +1,69 @@
package promql
import (
"fmt"
"strings"
"testing"
)
func TestWalk(t *testing.T) {
e, err := testParser.ParseRawPromQL(`
WITH (
rf(a, b) = a + b
)
rf(metric1{foo="bar"}, metric2) or (sum(abs(changes(metric3))))
`)
if err != nil {
t.Fatalf("unexpected error when parsing: %s", err)
}
var cv collectVisitor
Walk(e, &cv)
expected := []string{
`*promql.WithExpr WITH (rf(a,b) = a + b) rf(metric1{foo="bar"}, metric2) or (sum(abs(changes(metric3))))`,
`*promql.WithArgExpr rf(a,b) = a + b`,
`*promql.BinaryOpExpr a + b`,
`*promql.MetricTemplateExpr a`,
`*promql.TagFilterExpr __name__="a"`,
`*promql.StringTemplateExpr "a"`,
`*promql.MetricTemplateExpr b`,
`*promql.TagFilterExpr __name__="b"`,
`*promql.StringTemplateExpr "b"`,
`*promql.BinaryOpExpr rf(metric1{foo="bar"}, metric2) or (sum(abs(changes(metric3))))`,
`*promql.FuncExpr rf(metric1{foo="bar"}, metric2)`,
`*promql.MetricTemplateExpr metric1{foo="bar"}`,
`*promql.TagFilterExpr __name__="metric1"`,
`*promql.StringTemplateExpr "metric1"`,
`*promql.TagFilterExpr foo="bar"`,
`*promql.StringTemplateExpr "bar"`,
`*promql.MetricTemplateExpr metric2`,
`*promql.TagFilterExpr __name__="metric2"`,
`*promql.StringTemplateExpr "metric2"`,
`*promql.ParensExpr (sum(abs(changes(metric3))))`,
`*promql.AggrFuncExpr sum(abs(changes(metric3)))`,
`*promql.FuncExpr abs(changes(metric3))`,
`*promql.FuncExpr changes(metric3)`,
`*promql.MetricTemplateExpr metric3`,
`*promql.TagFilterExpr __name__="metric3"`,
`*promql.StringTemplateExpr "metric3"`,
`*promql.ModifierExpr ()`,
}
if len(cv.visited) != len(expected) {
t.Fatal("Expected", len(expected), "elements visited, got", len(cv.visited))
}
for i, v := range cv.visited {
if strings.TrimSpace(v) != expected[i] {
t.Fatalf("Expected %s, got %s at position %v", expected[i], v, i)
}
}
}
type collectVisitor struct {
visited []string
}
func (cv *collectVisitor) Visit(e Expr) Visitor {
sb := e.AppendString(nil)
s := fmt.Sprintf("%T %v\n", e, string(sb))
cv.visited = append(cv.visited, s)
return cv
}