package utils
import (
"fmt"
"math"
"regexp"
"slices"
"strconv"
"strings"
"github.com/prometheus/prometheus/model/labels"
promParser "github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/promql/parser/posrange"
)
var guaranteedLabelsMatches = []labels.MatchType{labels.MatchEqual, labels.MatchRegexp}
type SourceType int
const (
UnknownSource SourceType = iota
NumberSource
StringSource
SelectorSource
FuncSource
AggregateSource
)
type ExcludedLabel struct {
Reason string
Fragment posrange.PositionRange
}
type Join struct {
Src Source
}
// FIXME remove Selector/Call/Aggregation?
// Use a single parser.Node instead?
type Source struct {
Selector *promParser.VectorSelector // Vector selector used for this source.
Call *promParser.Call // Most outer call used inside this source.
Aggregation *promParser.AggregateExpr // Most outer aggregation expression used inside this source.
ExcludeReason map[string]ExcludedLabel // Reason why a label was excluded
Operation string
IsDeadReason string
Returns promParser.ValueType
Joins []Join // Any other sources this source joins with.
Unless []Join // Any other sources this source is suppressed by.
IncludedLabels []string // Labels that are included by filters, they will be present if exist on source series (by).
ExcludedLabels []string // Labels guaranteed to be excluded from the results (without).
GuaranteedLabels []string // Labels guaranteed to be present on the results (matchers).
Position posrange.PositionRange
ReturnedNumber float64 // If AlwaysReturns=true this is the number that's returned
Type SourceType
FixedLabels bool // Labels are fixed and only allowed labels can be present.
IsDead bool // True if this source cannot be reached and is dead code.
AlwaysReturns bool // True if this source always returns results.
KnownReturn bool // True if we always know the return value.
IsConditional bool // True if this source is guarded by 'foo > 5' or other condition.
}
func (s Source) Fragment(expr string) string {
switch {
case s.Type == FuncSource && s.Call != nil:
return GetQueryFragment(expr, s.Call.PosRange)
case s.Call != nil:
return GetQueryFragment(expr, s.Call.PosRange)
case s.Type == AggregateSource && s.Aggregation != nil:
return GetQueryFragment(expr, s.Aggregation.PosRange)
case s.Selector != nil:
return GetQueryFragment(expr, s.Selector.PosRange)
default:
return ""
}
}
func (s Source) GetSmallestPosition() (pr posrange.PositionRange) {
pr.Start = s.Position.Start
pr.End = s.Position.End
if s.Selector != nil {
if s.Selector.PosRange.Start > pr.Start {
pr.Start = s.Selector.PosRange.Start
pr.End = s.Selector.PosRange.End
}
}
if s.Call != nil {
if s.Call.PosRange.Start > pr.Start {
pr.Start = s.Call.PosRange.Start
pr.End = s.Call.PosRange.End
}
}
if s.Aggregation != nil {
if s.Aggregation.PosRange.Start > pr.Start {
pr.Start = s.Aggregation.PosRange.Start
pr.End = s.Aggregation.PosRange.End
}
}
return pr
}
func (s Source) CanHaveLabel(name string) bool {
if slices.Contains(s.ExcludedLabels, name) {
return false
}
if slices.Contains(s.IncludedLabels, name) {
return true
}
if slices.Contains(s.GuaranteedLabels, name) {
return true
}
return !s.FixedLabels
}
func (s Source) LabelExcludeReason(name string) ExcludedLabel {
if el, ok := s.ExcludeReason[name]; ok {
return el
}
return s.ExcludeReason[""]
}
type Visitor func(s Source)
func (s Source) WalkSources(fn Visitor) {
fn(s)
for _, j := range s.Joins {
j.Src.WalkSources(fn)
}
for _, u := range s.Unless {
u.Src.WalkSources(fn)
}
}
func LabelsSource(expr string, node promParser.Node) (src []Source) {
return walkNode(expr, node)
}
func walkNode(expr string, node promParser.Node) (src []Source) {
var s Source
switch n := node.(type) {
case *promParser.AggregateExpr:
src = append(src, walkAggregation(expr, n)...)
case *promParser.BinaryExpr:
src = append(src, parseBinOps(expr, n)...)
case *promParser.Call:
src = append(src, parseCall(expr, n)...)
case *promParser.MatrixSelector:
src = append(src, walkNode(expr, n.VectorSelector)...)
case *promParser.SubqueryExpr:
src = append(src, walkNode(expr, n.Expr)...)
case *promParser.NumberLiteral:
s.Type = NumberSource
s.Returns = promParser.ValueTypeScalar
s.KnownReturn = true
s.ReturnedNumber = n.Val
s.IncludedLabels = nil
s.GuaranteedLabels = nil
s.FixedLabels = true
s.AlwaysReturns = true
s.ExcludeReason = setInMap(
s.ExcludeReason,
"",
ExcludedLabel{
Reason: "This query returns a number value with no labels.",
Fragment: n.PosRange,
},
)
s.Position = n.PosRange
src = append(src, s)
case *promParser.ParenExpr:
src = append(src, walkNode(expr, n.Expr)...)
case *promParser.StringLiteral:
s.Type = StringSource
s.Returns = promParser.ValueTypeString
s.IncludedLabels = nil
s.GuaranteedLabels = nil
s.FixedLabels = true
s.AlwaysReturns = true
s.ExcludeReason = setInMap(
s.ExcludeReason,
"",
ExcludedLabel{
Reason: "This query returns a string value with no labels.",
Fragment: n.PosRange,
},
)
s.Position = n.PosRange
src = append(src, s)
case *promParser.UnaryExpr:
src = append(src, walkNode(expr, n.Expr)...)
case *promParser.StepInvariantExpr:
// Not possible to get this from the parser.
case *promParser.VectorSelector:
s.Type = SelectorSource
s.Returns = promParser.ValueTypeVector
s.Selector = n
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, n)...)
for _, name := range labelsWithEmptyValueSelector(n) {
s = excludeLabel(s, name)
s.ExcludeReason = setInMap(
s.ExcludeReason,
name,
ExcludedLabel{
Reason: fmt.Sprintf("Query uses `{%s=\"\"}` selector which will filter out any time series with the `%s` label set.", name, name),
Fragment: n.PosRange,
},
)
}
s.Position = n.PosRange
src = append(src, s)
default:
// unhandled type
}
return src
}
func removeFromSlice(sl []string, s ...string) []string {
for _, v := range s {
idx := slices.Index(sl, v)
if idx >= 0 {
if len(sl) == 1 {
return nil
}
sl = slices.Delete(sl, idx, idx+1)
}
}
return sl
}
func appendToSlice(dst []string, values ...string) []string {
for _, v := range values {
if !slices.Contains(dst, v) {
dst = append(dst, v)
}
}
return dst
}
func includeLabel(s Source, names ...string) Source {
s.ExcludedLabels = removeFromSlice(s.ExcludedLabels, names...)
for _, name := range names {
delete(s.ExcludeReason, name)
}
s.IncludedLabels = appendToSlice(s.IncludedLabels, names...)
return s
}
// Include labels that were not already excluded.
// FIXME most should use this?
func maybeIncludeLabel(s Source, names ...string) Source {
for _, name := range names {
if !slices.Contains(s.ExcludedLabels, name) {
s.IncludedLabels = appendToSlice(s.IncludedLabels, names...)
}
}
return s
}
func restrictIncludedLabels(s Source, names []string) Source {
todo := []string{}
for _, name := range s.IncludedLabels {
if !slices.Contains(names, name) {
todo = append(todo, name)
}
}
s.IncludedLabels = removeFromSlice(s.IncludedLabels, todo...)
return s
}
func guaranteeLabel(s Source, names ...string) Source {
s.ExcludedLabels = removeFromSlice(s.ExcludedLabels, names...)
for _, name := range names {
delete(s.ExcludeReason, name)
}
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, names...)
return s
}
func restrictGuaranteedLabels(s Source, names []string) Source {
todo := []string{}
for _, name := range s.GuaranteedLabels {
if !slices.Contains(names, name) {
todo = append(todo, name)
}
}
s.GuaranteedLabels = removeFromSlice(s.GuaranteedLabels, todo...)
return s
}
func excludeLabel(s Source, names ...string) Source {
s.ExcludedLabels = appendToSlice(s.ExcludedLabels, names...)
s.IncludedLabels = removeFromSlice(s.IncludedLabels, names...)
s.GuaranteedLabels = removeFromSlice(s.GuaranteedLabels, names...)
return s
}
func setInMap(dst map[string]ExcludedLabel, key string, val ExcludedLabel) map[string]ExcludedLabel {
if dst == nil {
dst = map[string]ExcludedLabel{}
}
dst[key] = val
return dst
}
func labelsFromSelectors(matches []labels.MatchType, selector *promParser.VectorSelector) (names []string) {
if selector == nil {
return nil
}
// Any label used in positive filters is gurnateed to be present.
for _, lm := range selector.LabelMatchers {
if lm.Name == labels.MetricName {
continue
}
if !slices.Contains(matches, lm.Type) {
continue
}
names = appendToSlice(names, lm.Name)
}
return names
}
func labelsWithEmptyValueSelector(selector *promParser.VectorSelector) (names []string) {
for _, lm := range selector.LabelMatchers {
if lm.Name == labels.MetricName {
continue
}
if lm.Type == labels.MatchEqual && lm.Value == "" {
names = appendToSlice(names, lm.Name)
}
}
return names
}
func GetQueryFragment(expr string, pos posrange.PositionRange) string {
return expr[pos.Start:pos.End]
}
// FIXME Aggregations strip __name__.
func walkAggregation(expr string, n *promParser.AggregateExpr) (src []Source) {
var s Source
switch n.Op {
case promParser.SUM:
for _, s = range parseAggregation(expr, n) {
s.Aggregation = n
s.Operation = "sum"
s.Position = n.PosRange
src = append(src, s)
}
case promParser.MIN:
for _, s = range parseAggregation(expr, n) {
s.Aggregation = n
s.Operation = "min"
s.Position = n.PosRange
src = append(src, s)
}
case promParser.MAX:
for _, s = range parseAggregation(expr, n) {
s.Aggregation = n
s.Operation = "max"
s.Position = n.PosRange
src = append(src, s)
}
case promParser.AVG:
for _, s = range parseAggregation(expr, n) {
s.Aggregation = n
s.Operation = "avg"
s.Position = n.PosRange
src = append(src, s)
}
case promParser.GROUP:
for _, s = range parseAggregation(expr, n) {
s.Aggregation = n
s.Operation = "group"
s.Position = n.PosRange
src = append(src, s)
}
case promParser.STDDEV:
for _, s = range parseAggregation(expr, n) {
s.Aggregation = n
s.Operation = "stddev"
s.Position = n.PosRange
src = append(src, s)
}
case promParser.STDVAR:
for _, s = range parseAggregation(expr, n) {
s.Aggregation = n
s.Operation = "stdvar"
s.Position = n.PosRange
src = append(src, s)
}
case promParser.COUNT:
for _, s = range parseAggregation(expr, n) {
s.Aggregation = n
s.Operation = "count"
s.Position = n.PosRange
src = append(src, s)
}
case promParser.COUNT_VALUES:
for _, s = range parseAggregation(expr, n) {
s.Aggregation = n
s.Operation = "count_values"
// Param is the label to store the count value in.
s = includeLabel(s, n.Param.(*promParser.StringLiteral).Val)
s = guaranteeLabel(s, n.Param.(*promParser.StringLiteral).Val)
s.Position = n.PosRange
src = append(src, s)
}
case promParser.QUANTILE:
for _, s = range parseAggregation(expr, n) {
s.Aggregation = n
s.Operation = "quantile"
s.Position = n.PosRange
src = append(src, s)
}
case promParser.TOPK:
for _, s = range walkNode(expr, n.Expr) {
s.Type = AggregateSource
s.Aggregation = n
s.Operation = "topk"
s.Position = n.PosRange
src = append(src, s)
}
case promParser.BOTTOMK:
for _, s = range walkNode(expr, n.Expr) {
s.Type = AggregateSource
s.Aggregation = n
s.Operation = "bottomk"
s.Position = n.PosRange
src = append(src, s)
}
/*
TODO these are experimental and promParser.EnableExperimentalFunctions must be set to true to enable parsing of these.
case promParser.LIMITK:
s = walkNode(expr, n.Expr)
s.Type = AggregateSource
s.Operation = "limitk"
case promParser.LIMIT_RATIO:
s = walkNode(expr, n.Expr)
s.Type = AggregateSource
s.Operation = "limit_ratio"
*/
}
return src
}
func parseAggregation(expr string, n *promParser.AggregateExpr) (src []Source) {
var s Source
for _, s = range walkNode(expr, n.Expr) {
if n.Without {
s = excludeLabel(s, n.Grouping...)
for _, name := range n.Grouping {
s.ExcludeReason = setInMap(
s.ExcludeReason,
name,
ExcludedLabel{
Reason: fmt.Sprintf("Query is using aggregation with `without(%s)`, all labels included inside `without(...)` will be removed from the results.",
strings.Join(n.Grouping, ", ")),
Fragment: FindPosition(expr, n.PosRange, "without"),
},
)
}
} else {
if len(n.Grouping) == 0 {
s.IncludedLabels = nil
s.GuaranteedLabels = nil
s.ExcludeReason = setInMap(
s.ExcludeReason,
"",
ExcludedLabel{
Reason: "Query is using aggregation that removes all labels.",
Fragment: FindPosition(expr, n.PosRange, "sum"),
},
)
} else {
// Check if source of labels already fixes them.
if !s.FixedLabels {
s = maybeIncludeLabel(s, n.Grouping...)
s.ExcludeReason = setInMap(
s.ExcludeReason,
"",
ExcludedLabel{
Reason: fmt.Sprintf("Query is using aggregation with `by(%s)`, only labels included inside `by(...)` will be present on the results.",
strings.Join(n.Grouping, ", ")),
Fragment: FindPosition(expr, n.PosRange, "by"),
},
)
}
s = restrictGuaranteedLabels(s, n.Grouping)
s = restrictIncludedLabels(s, n.Grouping)
}
s.FixedLabels = true
}
s.Type = AggregateSource
s.Returns = promParser.ValueTypeVector
src = append(src, s)
}
return src
}
func parsePromQLFunc(s Source, expr string, n *promParser.Call) Source {
switch n.Func.Name {
case "abs", "sgn", "acos", "acosh", "asin", "asinh", "atan", "atanh", "cos", "cosh", "sin", "sinh", "tan", "tanh":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, s.Selector)...)
case "ceil", "floor", "round":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, s.Selector)...)
case "changes", "resets":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, s.Selector)...)
case "clamp", "clamp_max", "clamp_min":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, s.Selector)...)
case "absent", "absent_over_time":
s.Returns = promParser.ValueTypeVector
s.FixedLabels = true
s.IncludedLabels = nil
s.GuaranteedLabels = nil
for _, name := range labelsFromSelectors([]labels.MatchType{labels.MatchEqual}, s.Selector) {
s = includeLabel(s, name)
s = guaranteeLabel(s, name)
}
s.ExcludeReason = setInMap(
s.ExcludeReason,
"",
ExcludedLabel{
Reason: fmt.Sprintf(`The [%s()](https://prometheus.io/docs/prometheus/latest/querying/functions/#%s) function is used to check if provided query doesn't match any time series.
You will only get any results back if the metric selector you pass doesn't match anything.
Since there are no matching time series there are also no labels. If some time series is missing you cannot read its labels.
This means that the only labels you can get back from absent call are the ones you pass to it.
If you're hoping to get instance specific labels this way and alert when some target is down then that won't work, use the `+"`up`"+` metric instead.`,
n.Func.Name, n.Func.Name),
Fragment: FindPosition(expr, n.PosRange, n.Func.Name),
},
)
case "avg_over_time", "count_over_time", "last_over_time", "max_over_time", "min_over_time", "present_over_time", "quantile_over_time", "stddev_over_time", "stdvar_over_time", "sum_over_time":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, s.Selector)...)
case "days_in_month", "day_of_month", "day_of_week", "day_of_year", "hour", "minute", "month", "year":
s.Returns = promParser.ValueTypeVector
// No labels if we don't pass any arguments.
// Otherwise no change to labels.
if len(s.Call.Args) == 0 {
s.FixedLabels = true
s.AlwaysReturns = true
s.IncludedLabels = nil
s.GuaranteedLabels = nil
s.ExcludeReason = setInMap(
s.ExcludeReason,
"",
ExcludedLabel{
Reason: fmt.Sprintf("Calling `%s()` with no arguments will return an empty time series with no labels.",
n.Func.Name),
Fragment: n.PosRange,
},
)
} else {
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, s.Selector)...)
}
case "deg", "rad", "ln", "log10", "log2", "sqrt", "exp":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, s.Selector)...)
case "delta", "idelta", "increase", "deriv", "irate", "rate":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, s.Selector)...)
case "histogram_avg", "histogram_count", "histogram_sum", "histogram_stddev", "histogram_stdvar", "histogram_fraction", "histogram_quantile":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, s.Selector)...)
case "holt_winters", "predict_linear":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, s.Selector)...)
case "label_replace", "label_join":
// One label added to the results.
s.Returns = promParser.ValueTypeVector
s = guaranteeLabel(s, n.Args[1].(*promParser.StringLiteral).Val)
case "pi":
s.Returns = promParser.ValueTypeScalar
s.IncludedLabels = nil
s.GuaranteedLabels = nil
s.FixedLabels = true
s.AlwaysReturns = true
s.ExcludeReason = setInMap(
s.ExcludeReason,
"",
ExcludedLabel{
Reason: fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
Fragment: n.PosRange,
},
)
case "scalar":
s.Returns = promParser.ValueTypeScalar
s.IncludedLabels = nil
s.GuaranteedLabels = nil
s.FixedLabels = true
s.AlwaysReturns = true
s.ExcludeReason = setInMap(
s.ExcludeReason,
"",
ExcludedLabel{
Reason: fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
Fragment: FindPosition(expr, n.PositionRange(), n.Func.Name),
},
)
case "sort", "sort_desc":
// No change to labels.
s.Returns = promParser.ValueTypeVector
case "time":
s.Returns = promParser.ValueTypeScalar
s.IncludedLabels = nil
s.GuaranteedLabels = nil
s.FixedLabels = true
s.AlwaysReturns = true
s.ExcludeReason = setInMap(
s.ExcludeReason,
"",
ExcludedLabel{
Reason: fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
Fragment: n.PosRange,
},
)
case "timestamp":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s = guaranteeLabel(s, labelsFromSelectors(guaranteedLabelsMatches, s.Selector)...)
case "vector":
s.Returns = promParser.ValueTypeVector
s.IncludedLabels = nil
s.GuaranteedLabels = nil
s.FixedLabels = true
s.AlwaysReturns = true
for _, vs := range walkNode(expr, n.Args[0]) {
if vs.KnownReturn {
s.ReturnedNumber = vs.ReturnedNumber
s.KnownReturn = true
}
}
s.ExcludeReason = setInMap(
s.ExcludeReason,
"",
ExcludedLabel{
Reason: fmt.Sprintf("Calling `%s()` will return a vector value with no labels.", n.Func.Name),
Fragment: FindPosition(expr, n.PosRange, n.Func.Name),
},
)
default:
// Unsupported function
s.Returns = promParser.ValueTypeNone
s.Call = nil
}
return s
}
func parseCall(expr string, n *promParser.Call) (src []Source) {
var vt promParser.ValueType
for i, e := range n.Args {
if i >= len(n.Func.ArgTypes) {
vt = n.Func.ArgTypes[len(n.Func.ArgTypes)-1]
} else {
vt = n.Func.ArgTypes[i]
}
switch vt {
case promParser.ValueTypeVector, promParser.ValueTypeMatrix:
for _, es := range walkNode(expr, e) {
es.Type = FuncSource
es.Operation = n.Func.Name
es.Call = n
src = append(src, parsePromQLFunc(es, expr, n))
}
case promParser.ValueTypeNone, promParser.ValueTypeScalar, promParser.ValueTypeString:
}
}
if len(src) == 0 {
var s Source
s.Type = FuncSource
s.Operation = n.Func.Name
s.Call = n
s.Position = n.PosRange
src = append(src, parsePromQLFunc(s, expr, n))
}
return src
}
func parseBinOps(expr string, n *promParser.BinaryExpr) (src []Source) {
var s Source
switch {
// foo{} + 1
// 1 + foo{}
// foo{} > 1
// 1 > foo{}
case n.VectorMatching == nil:
lhs := walkNode(expr, n.LHS)
rhs := walkNode(expr, n.RHS)
for _, ls := range lhs {
ls.IsConditional = isConditional(ls, n.Op)
for _, rs := range rhs {
rs.IsConditional = isConditional(rs, n.Op)
var side Source
switch {
case ls.Returns == promParser.ValueTypeVector, ls.Returns == promParser.ValueTypeMatrix:
// Use labels from LHS
side = ls
case rs.Returns == promParser.ValueTypeVector, rs.Returns == promParser.ValueTypeMatrix:
// Use labels from RHS
side = rs
default:
side = ls
}
if ls.AlwaysReturns && rs.AlwaysReturns && ls.KnownReturn && rs.KnownReturn {
// Both sides always return something
side.ReturnedNumber, side.IsDead, side.IsDeadReason = calculateStaticReturn(
expr,
ls, rs,
n.Op,
ls.IsDead,
)
}
src = append(src, side)
}
}
// foo{} + bar{}
// foo{} + on(...) bar{}
// foo{} + ignoring(...) bar{}
// foo{} / bar{}
case n.VectorMatching.Card == promParser.CardOneToOne:
rhs := walkNode(expr, n.RHS)
for _, s = range walkNode(expr, n.LHS) {
if n.VectorMatching.On {
s.FixedLabels = true
s = includeLabel(s, n.VectorMatching.MatchingLabels...)
s = restrictIncludedLabels(s, n.VectorMatching.MatchingLabels)
s = restrictGuaranteedLabels(s, n.VectorMatching.MatchingLabels)
s.ExcludeReason = setInMap(
s.ExcludeReason,
"",
ExcludedLabel{
Reason: fmt.Sprintf(
"Query is using %s vector matching with `on(%s)`, only labels included inside `on(...)` will be present on the results.",
n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
),
Fragment: FindPosition(expr, n.PositionRange(), "on"),
},
)
} else {
s = excludeLabel(s, n.VectorMatching.MatchingLabels...)
for _, name := range n.VectorMatching.MatchingLabels {
s.ExcludeReason = setInMap(
s.ExcludeReason,
name,
ExcludedLabel{
Reason: fmt.Sprintf(
"Query is using %s vector matching with `ignoring(%s)`, all labels included inside `ignoring(...)` will be removed on the results.",
n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
),
Fragment: FindPosition(expr, n.PositionRange(), "ignoring"),
},
)
}
for _, rs := range rhs {
rs.IsConditional = isConditional(rs, n.Op)
if s.AlwaysReturns && rs.AlwaysReturns && s.KnownReturn && rs.KnownReturn {
// Both sides always return something
s.ReturnedNumber, s.IsDead, s.IsDeadReason = calculateStaticReturn(
expr,
s, rs,
n.Op,
s.IsDead,
)
}
}
}
if s.Operation == "" {
s.Operation = n.VectorMatching.Card.String()
}
for _, rs := range rhs {
if ok, s := canJoin(s, rs, n.VectorMatching); !ok {
rs.IsDead = true
rs.IsDeadReason = s
}
s.Joins = append(s.Joins, Join{
Src: rs,
})
}
s.IsConditional = isConditional(s, n.Op)
src = append(src, s)
}
// foo{} + on(...) group_right(...) bar{}
// foo{} + ignoring(...) group_right(...) bar{}
case n.VectorMatching.Card == promParser.CardOneToMany:
lhs := walkNode(expr, n.LHS)
for _, s = range walkNode(expr, n.RHS) {
s = includeLabel(s, n.VectorMatching.Include...)
// If we have:
// foo * on(instance) group_left(a,b) bar{x="y"}
// then only group_left() labels will be included.
if n.VectorMatching.On {
s = includeLabel(s, n.VectorMatching.MatchingLabels...)
}
if s.Operation == "" {
s.Operation = n.VectorMatching.Card.String()
}
for _, ls := range lhs {
if ok, s := canJoin(s, ls, n.VectorMatching); !ok {
ls.IsDead = true
ls.IsDeadReason = s
}
s.Joins = append(s.Joins, Join{
Src: ls,
})
}
s.IsConditional = isConditional(s, n.Op)
src = append(src, s)
}
// foo{} + on(...) group_left(...) bar{}
// foo{} + ignoring(...) group_left(...) bar{}
case n.VectorMatching.Card == promParser.CardManyToOne:
rhs := walkNode(expr, n.RHS)
for _, s = range walkNode(expr, n.LHS) {
s = includeLabel(s, n.VectorMatching.Include...)
if n.VectorMatching.On {
s = includeLabel(s, n.VectorMatching.MatchingLabels...)
}
if s.Operation == "" {
s.Operation = n.VectorMatching.Card.String()
}
for _, rs := range rhs {
if ok, s := canJoin(s, rs, n.VectorMatching); !ok {
rs.IsDead = true
rs.IsDeadReason = s
}
s.Joins = append(s.Joins, Join{
Src: rs,
})
}
s.IsConditional = isConditional(s, n.Op)
src = append(src, s)
}
// foo{} and on(...) bar{}
// foo{} and ignoring(...) bar{}
// foo{} unless bar{}
case n.VectorMatching.Card == promParser.CardManyToMany:
var lhsCanBeEmpty bool // true if any of the LHS query can produce empty results.
rhs := walkNode(expr, n.RHS)
for _, s = range walkNode(expr, n.LHS) {
var rhsConditional bool
if n.VectorMatching.On {
s = includeLabel(s, n.VectorMatching.MatchingLabels...)
}
if s.Operation == "" {
s.Operation = n.VectorMatching.Card.String()
}
if !s.AlwaysReturns || s.IsConditional {
lhsCanBeEmpty = true
}
for _, rs := range rhs {
if isConditional(rs, n.Op) {
rhsConditional = true
}
if ok, s := canJoin(s, rs, n.VectorMatching); !ok {
rs.IsDead = true
rs.IsDeadReason = s
}
switch {
case n.Op == promParser.LUNLESS:
if n.VectorMatching.On && len(n.VectorMatching.MatchingLabels) == 0 && rs.AlwaysReturns && !rs.IsConditional {
s.IsDead = true
s.IsDeadReason = "this query will never return anything because the `unless` query always returns something"
}
s.Unless = append(s.Unless, Join{
Src: rs,
})
case n.Op != promParser.LOR:
s.Joins = append(s.Joins, Join{
Src: rs,
})
}
}
if n.Op == promParser.LAND && rhsConditional {
s.IsConditional = true
}
src = append(src, s)
}
if n.Op == promParser.LOR {
for _, s = range rhs {
if s.Operation == "" {
s.Operation = n.VectorMatching.Card.String()
}
// If LHS can NOT be empty then RHS is dead code.
if !lhsCanBeEmpty {
s.IsDead = true
s.IsDeadReason = "the left hand side always returs something and so the right hand side is never used"
}
src = append(src, s)
}
}
}
return src
}
func isConditional(s Source, op promParser.ItemType) bool {
if s.IsConditional {
return true
}
return op.IsComparisonOperator()
}
func canJoin(ls, rs Source, vm *promParser.VectorMatching) (bool, string) {
var side string
if vm.Card == promParser.CardOneToMany {
side = "left"
} else {
side = "right"
}
switch {
case vm.On && len(vm.MatchingLabels) == 0: // ls on() unless rs
return true, ""
case vm.On: // ls on(...) unless rs
for _, name := range vm.MatchingLabels {
if ls.CanHaveLabel(name) && !rs.CanHaveLabel(name) {
return false, fmt.Sprintf("The %s hand side will never be matched because it doesn't have the `%s` label from `on(...)`. %s",
side, name, rs.LabelExcludeReason(name).Reason)
}
}
default: // ls unless rs
for _, name := range ls.GuaranteedLabels {
if ls.CanHaveLabel(name) && !rs.CanHaveLabel(name) {
return false, fmt.Sprintf("The %s hand side will never be matched because it doesn't have the `%s` label while the left hand side will. %s",
side, name, rs.LabelExcludeReason(name).Reason)
}
}
}
return true, ""
}
func ftos(v float64) string {
return strconv.FormatFloat(v, 'f', -1, 64)
}
func calculateStaticReturn(expr string, ls, rs Source, op promParser.ItemType, isDead bool) (float64, bool, string) {
lf := ls.Fragment(expr)
rf := rs.Fragment(expr)
var cmpPrefix string
if lf != "" && rf != "" {
cmpPrefix = fmt.Sprintf("`%s %s %s` always evaluates to", lf, op, rf)
} else {
cmpPrefix = "this query always evaluates to"
}
cmpSuffix := "which is not possible, so it will never return anything"
switch op {
case promParser.EQLC:
if ls.ReturnedNumber != rs.ReturnedNumber {
return ls.ReturnedNumber, true, fmt.Sprintf("%s `%s == %s` %s", cmpPrefix, ftos(ls.ReturnedNumber), ftos(rs.ReturnedNumber), cmpSuffix)
}
case promParser.NEQ:
if ls.ReturnedNumber == rs.ReturnedNumber {
return ls.ReturnedNumber, true, fmt.Sprintf("%s `%s != %s` %s", cmpPrefix, ftos(ls.ReturnedNumber), ftos(rs.ReturnedNumber), cmpSuffix)
}
case promParser.LTE:
if ls.ReturnedNumber > rs.ReturnedNumber {
return ls.ReturnedNumber, true, fmt.Sprintf("%s `%s <= %s` %s", cmpPrefix, ftos(ls.ReturnedNumber), ftos(rs.ReturnedNumber), cmpSuffix)
}
case promParser.LSS:
if ls.ReturnedNumber >= rs.ReturnedNumber {
return ls.ReturnedNumber, true, fmt.Sprintf("%s `%s < %s` %s", cmpPrefix, ftos(ls.ReturnedNumber), ftos(rs.ReturnedNumber), cmpSuffix)
}
case promParser.GTE:
if ls.ReturnedNumber < rs.ReturnedNumber {
return ls.ReturnedNumber, true, fmt.Sprintf("%s `%s >= %s` %s", cmpPrefix, ftos(ls.ReturnedNumber), ftos(rs.ReturnedNumber), cmpSuffix)
}
case promParser.GTR:
if ls.ReturnedNumber <= rs.ReturnedNumber {
return ls.ReturnedNumber, true, fmt.Sprintf("%s `%s > %s` %s", cmpPrefix, ftos(ls.ReturnedNumber), ftos(rs.ReturnedNumber), cmpSuffix)
}
case promParser.ADD:
return ls.ReturnedNumber + rs.ReturnedNumber, isDead, ""
case promParser.SUB:
return ls.ReturnedNumber - rs.ReturnedNumber, isDead, ""
case promParser.MUL:
return ls.ReturnedNumber * rs.ReturnedNumber, isDead, ""
case promParser.DIV:
return ls.ReturnedNumber / rs.ReturnedNumber, isDead, ""
case promParser.MOD:
return math.Mod(ls.ReturnedNumber, rs.ReturnedNumber), isDead, ""
case promParser.POW:
return math.Pow(ls.ReturnedNumber, rs.ReturnedNumber), isDead, ""
}
return ls.ReturnedNumber, isDead, ""
}
// FIXME sum() on ().
func FindPosition(expr string, within posrange.PositionRange, fn string) posrange.PositionRange {
re := regexp.MustCompile("(?i)(" + regexp.QuoteMeta(fn) + ")[ \n\t]*\\(")
idx := re.FindStringSubmatchIndex(GetQueryFragment(expr, within))
if idx == nil {
return within
}
return posrange.PositionRange{
Start: within.Start + posrange.Pos(idx[0]),
End: within.Start + posrange.Pos(idx[1]-1),
}
}cloudflare/pint
Publicmirrored from https://github.com/cloudflare/pintAvailable
internal/parser/utils/source.go
1048lines · modepreview