diff --git a/README.md b/README.md
index 6cf6477..4017cd9 100644
--- a/README.md
+++ b/README.md
@@ -87,20 +87,29 @@ Superpower.ParseException: Syntax error (line 1, column 21): Unexpected left bra
at Superpower.Tokenizer`1.Tokenize(String source)
at PromQL.Parser.Parser.ParseExpression(String input, Tokenizer tokenizer)
at UserQuery.Main() in C:\Users\james.luck\AppData\Local\Temp\LINQPad6\_aryncqeb\dwfjew\LINQPadQuery:line [[
-```
+```
-**Only syntax is validated, semantic validation is not applied**. For example, the following expressions
-are considered valid by this library:
+### Type checking
+Syntax is not only validated but expressions can also be typed checked via `CheckType`, e.g:
+```
+Parser.ParseExpression("1 + sum_over_time(some_metric[1h])").CheckType().Dump();
+```
+Returns:
+```
+ValueType.Vector
```
-# Invalid number of function arguments
-time(1, 2, 3, 4)
-
-# Invalid number and types of aggregation function arguments
-sum(1.0, "a string")
-# Binary operations not defined for strings
-"a" + "b"
+Expressions that violate the PromQL type system will throw an exception, e.g:
+```
+Parser.ParseExpression("'a' + 'b'").CheckType();
+```
+Throws:
+```
+Unexpected type 'string' was provided, expected one of 'scalar', 'instant vector': 0 (line 1, column 1)
+ at PromQL.Parser.TypeChecker.ExpectTypes(Expr expr, ValueType[] expected)
+ at PromQL.Parser.TypeChecker.CheckType(Expr expr)
+ at UserQuery.Main(), line 1
```
### Modifying PromQL expressions
@@ -161,7 +170,7 @@ This library aims to parse 99% of the PromQL language and has [extensive test co
- Hexadecimal/ octal string escaping
- Hexadecimal number representation
- [The `@` modifier](https://prometheus.io/docs/prometheus/latest/querying/basics/#modifier)
-- Operator associativity (all operators are parsed with the same associativity)
+- Exponential operator associativity (is parsed with left associativity)
If any of these features are important to you, please create an issue or submit a pull request.
diff --git a/src/PromQL.Parser/Ast.cs b/src/PromQL.Parser/Ast.cs
index 385f70e..3798b29 100644
--- a/src/PromQL.Parser/Ast.cs
+++ b/src/PromQL.Parser/Ast.cs
@@ -2,6 +2,8 @@
using System.Collections.Immutable;
using System.Text;
using ExhaustiveMatching;
+using Superpower.Model;
+using Superpower.Parsers;
namespace PromQL.Parser.Ast
{
@@ -11,6 +13,7 @@ namespace PromQL.Parser.Ast
public interface IPromQlNode
{
void Accept(IVisitor visitor);
+ TextSpan? Span { get; }
}
///
@@ -29,34 +32,39 @@ public interface IPromQlNode
typeof(UnaryExpr),
typeof(VectorSelector)
)]
- public interface Expr : IPromQlNode {}
+ public interface Expr : IPromQlNode
+ {
+ ValueType Type { get; }
+ }
///
/// Represents an aggregation operation on a Vector.
///
- /// The used aggregation operation.
+ /// The used aggregation operation.
/// The Vector expression over which is aggregated.
/// Parameter used by some aggregators.
/// The labels by which to group the Vector.
/// Whether to drop the given labels rather than keep them.
- public record AggregateExpr(string OperatorName, Expr Expr, Expr? Param,
- ImmutableArray GroupingLabels, bool Without) : Expr
+ public record AggregateExpr(AggregateOperator Operator, Expr Expr, Expr? Param,
+ ImmutableArray GroupingLabels, bool Without, TextSpan? Span = null) : Expr
{
- public AggregateExpr(string operatorName, Expr expr)
- : this (operatorName, expr, null, ImmutableArray.Empty, false)
+ public AggregateExpr(AggregateOperator @operator, Expr expr)
+ : this (@operator, expr, null, ImmutableArray.Empty, false)
{
}
- public AggregateExpr(string operatorName, Expr expr, Expr param, bool without = false, params string[] groupingLabels)
- : this (operatorName, expr, param, groupingLabels.ToImmutableArray(), without)
+ public AggregateExpr(AggregateOperator @operator, Expr expr, Expr param, bool without = false, params string[] groupingLabels)
+ : this (@operator, expr, param, groupingLabels.ToImmutableArray(), without)
{
}
- public string OperatorName { get; set;} = OperatorName;
+ public AggregateOperator Operator { get; set;} = Operator;
public Expr Expr { get; set;} = Expr;
public Expr? Param { get; set;} = Param;
public ImmutableArray GroupingLabels { get; set;} = GroupingLabels;
public bool Without { get; set;} = Without;
+
+ public ValueType Type => ValueType.Vector;
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
@@ -69,13 +77,24 @@ public AggregateExpr(string operatorName, Expr expr, Expr param, bool without =
/// The operation of the expression
/// The matching behavior for the operation to be applied if both operands are Vectors.
public record BinaryExpr(Expr LeftHandSide, Expr RightHandSide, Operators.Binary Operator,
- VectorMatching? VectorMatching = null) : Expr
+ VectorMatching? VectorMatching = null, TextSpan? Span = null) : Expr
{
public Expr LeftHandSide { get; set; } = LeftHandSide;
public Expr RightHandSide { get; set; } = RightHandSide;
public Operators.Binary Operator { get; set; } = Operator;
public VectorMatching? VectorMatching { get; set; } = VectorMatching;
public void Accept(IVisitor visitor) => visitor.Visit(this);
+
+ public ValueType Type
+ {
+ get
+ {
+ if (RightHandSide.Type == ValueType.Scalar && LeftHandSide.Type == ValueType.Scalar)
+ return ValueType.Scalar;
+
+ return ValueType.Vector;
+ }
+ }
}
///
@@ -87,7 +106,7 @@ public record BinaryExpr(Expr LeftHandSide, Expr RightHandSide, Operators.Binary
/// Contains additional labels that should be included in the result from the side with the lower cardinality.
/// If a comparison operator, return 0/1 rather than filtering.
public record VectorMatching(Operators.VectorMatchCardinality MatchCardinality, ImmutableArray MatchingLabels,
- bool On, ImmutableArray Include, bool ReturnBool) : IPromQlNode
+ bool On, ImmutableArray Include, bool ReturnBool, TextSpan? Span = null) : IPromQlNode
{
public static Operators.VectorMatchCardinality DefaultMatchCardinality { get; } = Operators.VectorMatchCardinality.OneToOne;
@@ -104,91 +123,104 @@ public VectorMatching(bool returnBool) : this (DefaultMatchCardinality, Immutabl
public bool On { get; set; } = On;
public ImmutableArray Include { get; set; } = Include;
public bool ReturnBool { get; set; } = ReturnBool;
-
+
public void Accept(IVisitor visitor) => visitor.Visit(this);
};
///
/// A function call.
///
- /// The function that was called.
+ /// The function that was called.
/// Arguments used in the call.
- public record FunctionCall(string Identifier, ImmutableArray Args) : Expr
+ public record FunctionCall(Function Function, ImmutableArray Args, TextSpan? Span = null) : Expr
{
- public FunctionCall(string identifier, params Expr[] args)
- : this (identifier, args.ToImmutableArray())
+ public FunctionCall(Function function, params Expr[] args)
+ : this (function, args.ToImmutableArray())
{
}
- public string Identifier { get; set; } = Identifier;
+ public Function Function { get; set; } = Function;
public ImmutableArray Args { get; set; } = Args;
-
- public void Accept(IVisitor visitor) => visitor.Visit(this);
+ public ValueType Type => Function.ReturnType;
+
+ public void Accept(IVisitor visitor) => visitor.Visit(this);
+
protected virtual bool PrintMembers(StringBuilder builder)
{
- builder.AppendLine($"{nameof(Identifier)} = {Identifier}, ");
+ builder.AppendLine($"{nameof(Function)} = {Function.Name}, ");
builder.Append($"{nameof(Args)} = ");
Args.PrintArray(builder);
-
+
return true;
}
}
- public record ParenExpression(Expr Expr) : Expr
+ public record ParenExpression(Expr Expr, TextSpan? Span = null) : Expr
{
public Expr Expr { get; set; } = Expr;
public void Accept(IVisitor visitor) => visitor.Visit(this);
+ public ValueType Type => Expr.Type;
}
- public record OffsetExpr(Expr Expr, Duration Duration) : Expr
+ public record OffsetExpr(Expr Expr, Duration Duration, TextSpan? Span = null) : Expr
{
public Expr Expr { get; set; } = Expr;
public Duration Duration { get; set; } = Duration;
public void Accept(IVisitor visitor) => visitor.Visit(this);
+ public ValueType Type => Expr.Type;
}
- public record MatrixSelector(VectorSelector Vector, Duration Duration) : Expr
+ public record MatrixSelector(VectorSelector Vector, Duration Duration, TextSpan? Span = null) : Expr
{
public VectorSelector Vector { get; set; } =Vector;
public Duration Duration { get; set; } = Duration;
public void Accept(IVisitor visitor) => visitor.Visit(this);
+ public ValueType Type => ValueType.Matrix;
}
- public record UnaryExpr(Operators.Unary Operator, Expr Expr) : Expr
+ public record UnaryExpr(Operators.Unary Operator, Expr Expr, TextSpan? Span = null) : Expr
{
public Operators.Unary Operator { get; set; } = Operator;
public Expr Expr { get; set; } = Expr;
public void Accept(IVisitor visitor) => visitor.Visit(this);
+ public ValueType Type => Expr.Type;
}
public record VectorSelector : Expr
{
- public VectorSelector(MetricIdentifier metricIdentifier)
+ public VectorSelector(MetricIdentifier metricIdentifier, TextSpan? span = null)
{
MetricIdentifier = metricIdentifier;
+ Span = span;
}
- public VectorSelector(LabelMatchers labelMatchers)
+ public VectorSelector(LabelMatchers labelMatchers, TextSpan? span = null)
{
LabelMatchers = labelMatchers;
+ Span = span;
}
- public VectorSelector(MetricIdentifier metricIdentifier, LabelMatchers labelMatchers)
+ public VectorSelector(MetricIdentifier metricIdentifier, LabelMatchers labelMatchers, TextSpan? span = null)
{
+
MetricIdentifier = metricIdentifier;
LabelMatchers = labelMatchers;
+ Span = span;
}
public MetricIdentifier? MetricIdentifier { get; set; }
public LabelMatchers? LabelMatchers { get; set; }
+ public TextSpan? Span { get; }
+ public ValueType Type => ValueType.Vector;
+
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
- public record LabelMatchers(ImmutableArray Matchers) : IPromQlNode
+ public record LabelMatchers(ImmutableArray Matchers, TextSpan? Span = null) : IPromQlNode
{
- protected virtual bool PrintMembers(StringBuilder builder)
+ protected virtual bool PrintMembers(System.Text.StringBuilder builder)
{
builder.Append($"{nameof(Matchers)} = ");
Matchers.PrintArray(builder);
@@ -201,43 +233,46 @@ protected virtual bool PrintMembers(StringBuilder builder)
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
- public record LabelMatcher(string LabelName, Operators.LabelMatch Operator, StringLiteral Value) : IPromQlNode
+ public record LabelMatcher(string LabelName, Operators.LabelMatch Operator, StringLiteral Value, TextSpan? Span = null) : IPromQlNode
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
- public record MetricIdentifier(string Value) : IPromQlNode
+ public record MetricIdentifier(string Value, TextSpan? Span = null) : IPromQlNode
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
- public record NumberLiteral(double Value) : Expr
+ public record NumberLiteral(double Value, TextSpan? Span = null) : Expr
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
+ public ValueType Type => ValueType.Scalar;
}
- public record Duration(TimeSpan Value) : IPromQlNode
+ public record Duration(TimeSpan Value, TextSpan? Span = null) : IPromQlNode
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
- public record StringLiteral(char Quote, string Value) : Expr
+ public record StringLiteral(char Quote, string Value, TextSpan? Span = null) : Expr
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
+ public ValueType Type => ValueType.String;
}
- public record SubqueryExpr(Expr Expr, Duration Range, Duration? Step = null) : Expr
+ public record SubqueryExpr(Expr Expr, Duration Range, Duration? Step = null, TextSpan? Span = null) : Expr
{
public Expr Expr { get; set; } = Expr;
public Duration Range { get; set; } = Range;
public Duration? Step { get; set; } = Step;
+ public ValueType Type => ValueType.Matrix;
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
internal static class Extensions
{
- internal static void PrintArray(this ImmutableArray arr, StringBuilder sb)
+ internal static void PrintArray(this ImmutableArray arr, System.Text.StringBuilder sb)
where T : notnull
{
sb.Append("[ ");
diff --git a/src/PromQL.Parser/Functions.cs b/src/PromQL.Parser/Functions.cs
index f64c9ca..de8c0d1 100644
--- a/src/PromQL.Parser/Functions.cs
+++ b/src/PromQL.Parser/Functions.cs
@@ -11,74 +11,86 @@ public static class Functions
/// Primarily taken from https://github.com/prometheus/prometheus/blob/main/web/ui/module/codemirror-promql/src/grammar/promql.grammar#L121-L188.
/// More authoritative source would be https://github.com/prometheus/prometheus/blob/7471208b5c8ff6b65b644adedf7eb964da3d50ae/promql/parser/functions.go.
///
- public static ImmutableHashSet Names = new[]
+ public static ImmutableDictionary Map { get; set; } = new[]
{
- "absent_over_time",
- "absent",
- "abs",
- "acos",
- "acosh",
- "asin",
- "asinh",
- "atan",
- "atanh",
- "avg_over_time",
- "ceil",
- "changes",
- "clamp",
- "clamp_max",
- "clamp_min",
- "cos",
- "cosh",
- "count_over_time",
- "days_in_month",
- "day_of_month",
- "day_of_week",
- "deg",
- "delta",
- "deriv",
- "exp",
- "floor",
- "histogram_quantile",
- "holt_winters",
- "hour",
- "idelta",
- "increase",
- "irate",
- "label_replace",
- "label_join",
- "last_over_time",
- "ln",
- "log_10",
- "log_2",
- "max_over_time",
- "min_over_time",
- "minute",
- "month",
- "pi",
- "predict_linear",
- "present_over_time",
- "quantile_over_time",
- "rad",
- "rate",
- "resets",
- "round",
- "scalar",
- "sgn",
- "sin",
- "sinh",
- "sort",
- "sort_desc",
- "sqrt",
- "stddev_over_time",
- "stdvar_over_time",
- "sum_over_time",
- "tan",
- "tanh",
- "timestamp",
- "time",
- "vector",
- "year"
- }.ToImmutableHashSet();
+ new Function("absent", ValueType.Vector, ValueType.Vector),
+ new Function("absent_over_time", ValueType.Vector, ValueType.Matrix),
+ new Function("abs", ValueType.Vector, ValueType.Vector),
+ new Function("acos", ValueType.Vector, ValueType.Vector),
+ new Function("acosh", ValueType.Vector, ValueType.Vector),
+ new Function("asin", ValueType.Vector, ValueType.Vector),
+ new Function("asinh", ValueType.Vector, ValueType.Vector),
+ new Function("atan", ValueType.Vector, ValueType.Vector),
+ new Function("atanh", ValueType.Vector, ValueType.Vector),
+ new Function("avg_over_time", ValueType.Vector, ValueType.Matrix),
+ new Function("ceil", ValueType.Vector, ValueType.Vector),
+ new Function("changes", ValueType.Vector, ValueType.Matrix),
+ new Function("clamp", ValueType.Vector, ValueType.Vector, ValueType.Scalar, ValueType.Scalar),
+ new Function("clamp_max", ValueType.Vector, ValueType.Vector, ValueType.Scalar),
+ new Function("clamp_min", ValueType.Vector, ValueType.Vector, ValueType.Scalar),
+ new Function("cos", ValueType.Vector, ValueType.Vector),
+ new Function("cosh", ValueType.Vector, ValueType.Vector),
+ new Function("count_over_time", ValueType.Vector, ValueType.Matrix),
+ new Function("days_in_month", ValueType.Vector, varadicModifier: 1 , ValueType.Vector),
+ new Function("day_of_month", ValueType.Vector, varadicModifier: 1, ValueType.Vector),
+ new Function("day_of_week", ValueType.Vector, varadicModifier: 1, ValueType.Vector),
+ new Function("deg", ValueType.Vector, ValueType.Vector),
+ new Function("delta", ValueType.Vector, ValueType.Matrix),
+ new Function("deriv", ValueType.Vector, ValueType.Matrix),
+ new Function("exp", ValueType.Vector, ValueType.Vector),
+ new Function("floor", ValueType.Vector, ValueType.Vector),
+ new Function("histogram_quantile", ValueType.Vector, ValueType.Scalar, ValueType.Vector),
+ new Function("holt_winters", ValueType.Vector, ValueType.Matrix, ValueType.Scalar, ValueType.Scalar),
+ new Function("hour", ValueType.Vector, varadicModifier: 1, ValueType.Vector),
+ new Function("idelta", ValueType.Vector, ValueType.Matrix),
+ new Function("increase", ValueType.Vector, ValueType.Matrix),
+ new Function("irate", ValueType.Vector, ValueType.Matrix),
+ new Function("label_replace", ValueType.Vector, ValueType.Vector, ValueType.String, ValueType.String, ValueType.String, ValueType.String),
+ new Function("label_join", ValueType.Vector, varadicModifier: 0, ValueType.Vector, ValueType.String, ValueType.String, ValueType.String),
+ new Function("last_over_time", ValueType.Vector, ValueType.Matrix),
+ new Function("ln", ValueType.Vector, ValueType.Vector),
+ new Function("log_10", ValueType.Vector, ValueType.Vector),
+ new Function("log_2", ValueType.Vector, ValueType.Vector),
+ new Function("max_over_time", ValueType.Vector, ValueType.Matrix),
+ new Function("min_over_time", ValueType.Vector, ValueType.Matrix),
+ new Function("minute", ValueType.Vector, varadicModifier: 1, ValueType.Vector),
+ new Function("month", ValueType.Vector, varadicModifier: 1, ValueType.Vector),
+ new Function("pi", ValueType.Scalar),
+ new Function("predict_linear", ValueType.Scalar, ValueType.Matrix, ValueType.Scalar),
+ new Function("present_over_time", ValueType.Vector, ValueType.Matrix),
+ new Function("quantile_over_time", ValueType.Vector, ValueType.Scalar, ValueType.Matrix),
+ new Function("rad", ValueType.Vector, ValueType.Vector),
+ new Function("rate", ValueType.Vector, ValueType.Matrix),
+ new Function("resets", ValueType.Vector, ValueType.Matrix),
+ new Function("round", ValueType.Vector, varadicModifier: 1, ValueType.Vector, ValueType.Scalar),
+ new Function("scalar", ValueType.Scalar, ValueType.Vector),
+ new Function("sgn", ValueType.Vector, ValueType.Vector),
+ new Function("sin", ValueType.Vector, ValueType.Vector),
+ new Function("sinh", ValueType.Vector, ValueType.Vector),
+ new Function("sort", ValueType.Vector, ValueType.Vector),
+ new Function("sort_desc", ValueType.Vector, ValueType.Vector),
+ new Function("sqrt", ValueType.Vector, ValueType.Vector),
+ new Function("stddev_over_time", ValueType.Vector, ValueType.Matrix),
+ new Function("stdvar_over_time", ValueType.Vector, ValueType.Matrix),
+ new Function("sum_over_time", ValueType.Vector, ValueType.Matrix),
+ new Function("tan", ValueType.Vector, ValueType.Vector),
+ new Function("tanh", ValueType.Vector, ValueType.Vector),
+ new Function("timestamp", ValueType.Vector, ValueType.Vector),
+ new Function("time", ValueType.Scalar),
+ new Function("vector", ValueType.Vector, ValueType.Scalar),
+ new Function("year", ValueType.Vector, varadicModifier: 1, ValueType.Vector)
+ }.ToImmutableDictionary(k => k.Name);
+ }
+
+ public record Function(string Name, ValueType ReturnType, ImmutableArray ArgTypes, int? VariadicModifier = null)
+ {
+ public Function(string name, ValueType returnType, params ValueType[] argTypes)
+ : this(name, returnType, argTypes.ToImmutableArray(), null) { }
+
+ public Function(string name, ValueType returnType, int varadicModifier, params ValueType[] argTypes)
+ : this(name, returnType, argTypes.ToImmutableArray(), varadicModifier) { }
+
+ public bool IsVariadic => VariadicModifier != null;
+ public int MinArgCount => ArgTypes.Length - (VariadicModifier ?? 0);
}
}
diff --git a/src/PromQL.Parser/Operators.cs b/src/PromQL.Parser/Operators.cs
index 3bf058a..19bdae5 100644
--- a/src/PromQL.Parser/Operators.cs
+++ b/src/PromQL.Parser/Operators.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Immutable;
+using System.Linq;
using ExhaustiveMatching;
namespace PromQL.Parser
@@ -16,26 +17,7 @@ public enum LabelMatch
Regexp,
NotRegexp
}
-
- ///
- /// Defines the set of all valid aggregator operators (e.g. sum, avg, etc.)
- ///
- public static ImmutableHashSet Aggregates = new []
- {
- "sum",
- "avg",
- "count",
- "min",
- "max",
- "group",
- "stddev",
- "stdvar",
- "topk",
- "bottomk",
- "count_values",
- "quantile",
- }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
-
+
///
/// Describes the cardinality relationship of two Vectors in a binary operation.
///
@@ -54,7 +36,7 @@ public enum VectorMatchCardinality
///
OneToMany
}
-
+
public enum Binary
{
Pow,
@@ -86,7 +68,64 @@ public enum Unary
///
Sub
}
-
+
+ ///
+ /// The set of binary operators that operate over sets (instant vectors) only.
+ ///
+ public static ImmutableHashSet BinarySetOperators { get; set; } = new []
+ {
+ Binary.And,
+ Binary.Or,
+ Binary.Unless
+ }.ToImmutableHashSet();
+
+ ///
+ /// The set of binary operations that compare expressions.
+ ///
+ /// https://github.com/prometheus/prometheus/blob/f103acd5135b8bbe885b17a73dafc7bbb586319c/promql/parser/lex.go#L71
+ public static ImmutableHashSet BinaryComparisonOperators { get; set; }= new[]
+ {
+ Binary.Gtr,
+ Binary.Gte,
+ Binary.Lss,
+ Binary.Lte,
+ Binary.Eql,
+ Binary.Neq
+ }.ToImmutableHashSet();
+
+ ///
+ /// Operators are ordered by highest -> lowest precedence.
+ ///
+ public static ImmutableArray> BinaryPrecedence { get; set; } = new[]
+ {
+ // TODO support right associativity for pow!
+ new[] { Binary.Pow },
+ new[] { Binary.Mul, Binary.Div, Binary.Atan2, Binary.Mod },
+ new[] { Binary.Add, Binary.Sub },
+ new[] { Binary.Eql, Binary.Neq, Binary.Gtr, Binary.Gte, Binary.Lss, Binary.Lte },
+ new[] { Binary.And, Binary.Unless },
+ new[] { Binary.Or }
+ }.Select(x => x.ToImmutableHashSet()).ToImmutableArray();
+
+ ///
+ /// Defines the set of all valid aggregator operators (e.g. sum, avg, etc.)
+ ///
+ public static ImmutableDictionary Aggregates { get; set; } = new []
+ {
+ new AggregateOperator("sum"),
+ new AggregateOperator("avg"),
+ new AggregateOperator("count"),
+ new AggregateOperator("min"),
+ new AggregateOperator("max"),
+ new AggregateOperator("group"),
+ new AggregateOperator("stddev"),
+ new AggregateOperator("stdvar"),
+ new AggregateOperator("topk", ValueType.Scalar),
+ new AggregateOperator("bottomk", ValueType.Scalar),
+ new AggregateOperator("count_values", ValueType.String),
+ new AggregateOperator("quantile", ValueType.Scalar)
+ }.ToImmutableDictionary(k => k.Name, v => v, StringComparer.OrdinalIgnoreCase);
+
public static string ToPromQl(this Operators.Binary op) => op switch
{
Operators.Binary.Add => "+",
@@ -132,4 +171,6 @@ public enum Unary
_ => throw ExhaustiveMatch.Failed(op)
};
}
+
+ public record AggregateOperator(string Name, ValueType? ParameterType = null);
}
diff --git a/src/PromQL.Parser/Parser.cs b/src/PromQL.Parser/Parser.cs
index e6067a1..89ca44f 100644
--- a/src/PromQL.Parser/Parser.cs
+++ b/src/PromQL.Parser/Parser.cs
@@ -20,15 +20,15 @@ namespace PromQL.Parser
///
public static class Parser
{
- public static TokenListParser UnaryOperator =
- Token.EqualTo(PromToken.ADD).Select(_ => Operators.Unary.Add).Or(
- Token.EqualTo(PromToken.SUB).Select(_ => Operators.Unary.Sub)
+ public static TokenListParser> UnaryOperator =
+ Token.EqualTo(PromToken.ADD).Select(t => Operators.Unary.Add.ToParsedValue(t.Span)).Or(
+ Token.EqualTo(PromToken.SUB).Select(t => Operators.Unary.Sub.ToParsedValue(t.Span))
);
public static TokenListParser UnaryExpr =
from op in Parse.Ref(() => UnaryOperator)
from expr in Parse.Ref(() => Expr)
- select new UnaryExpr(op, expr);
+ select new UnaryExpr(op.Value, expr, op.Span.UntilEnd(expr.Span));
private static IEnumerable FindTokensMatching(Func predicate) => typeof(PromToken).GetMembers()
.Select(enumMember => (enumMember, attr: enumMember.GetCustomAttributes(typeof(TokenAttribute), false).Cast().SingleOrDefault()))
@@ -46,9 +46,9 @@ private static IEnumerable FindTokensMatching(Func MetricIdentifier =
from id in Token.EqualTo(PromToken.METRIC_IDENTIFIER)
.Or(Token.EqualTo(PromToken.IDENTIFIER))
- .Or(Token.EqualTo(PromToken.AGGREGATE_OP).Where(t => Operators.Aggregates.Contains(t.ToStringValue()), "aggregate_op"))
+ .Or(Token.EqualTo(PromToken.AGGREGATE_OP).Where(t => Operators.Aggregates.ContainsKey(t.ToStringValue()), "aggregate_op"))
.Or(Token.Matching(t => AlphanumericOperatorTokens.Contains(t), "operator"))
- select new MetricIdentifier(id.ToStringValue());
+ select new MetricIdentifier(id.ToStringValue(), id.Span);
public static TokenListParser LabelMatchers =
from lb in Token.EqualTo(PromToken.LEFT_BRACE)
@@ -63,38 +63,40 @@ from comma in Token.EqualTo(PromToken.COMMA).Optional()
select new [] { matcherHead }.Concat(matcherTail)
).OptionalOrDefault(Array.Empty())
from rb in Token.EqualTo(PromToken.RIGHT_BRACE)
- select new LabelMatchers(matchers.ToImmutableArray());
+ select new LabelMatchers(matchers.ToImmutableArray(), lb.Span.UntilEnd(rb.Span));
public static TokenListParser VectorSelector =
(
from m in MetricIdentifier
from lm in LabelMatchers.AsNullable().OptionalOrDefault()
- select new VectorSelector(m, lm)
+ select new VectorSelector(m, lm, lm != null ? m.Span!.Value.UntilEnd(lm.Span) : m.Span!)
).Or(
from lm in LabelMatchers
- select new VectorSelector(lm)
+ select new VectorSelector(lm, lm.Span)
);
public static TokenListParser MatrixSelector =
from vs in VectorSelector
- from d in Parse.Ref(() => Duration).Between(Token.EqualTo(PromToken.LEFT_BRACKET), Token.EqualTo(PromToken.RIGHT_BRACKET))
- select new MatrixSelector(vs, d);
+ from lb in Token.EqualTo(PromToken.LEFT_BRACKET)
+ from d in Parse.Ref(() => Duration)
+ from rb in Token.EqualTo(PromToken.RIGHT_BRACKET)
+ select new MatrixSelector(vs, d, vs.Span!.Value.UntilEnd(rb.Span));
// TODO see https://github.com/prometheus/prometheus/blob/7471208b5c8ff6b65b644adedf7eb964da3d50ae/promql/parser/generated_parser.y#L679
- public static TokenListParser LabelValueMatcher =
+ public static TokenListParser> LabelValueMatcher =
from id in Token.EqualTo(PromToken.IDENTIFIER)
- .Or(Token.EqualTo(PromToken.AGGREGATE_OP).Where(x => Operators.Aggregates.Contains(x.ToStringValue())))
+ .Or(Token.EqualTo(PromToken.AGGREGATE_OP).Where(x => Operators.Aggregates.ContainsKey(x.ToStringValue())))
// Inside of grouping options label names can be recognized as keywords by the lexer. This is a list of keywords that could also be a label name.
// See https://github.com/prometheus/prometheus/blob/7471208b5c8ff6b65b644adedf7eb964da3d50ae/promql/parser/generated_parser.y#L678 for more info.
.Or(Token.Matching(t => KeywordAndAlphanumericOperatorTokens.Contains(t), "keyword_or_operator"))
.Or(Token.EqualTo(PromToken.OFFSET))
- select id.ToStringValue();
+ select new ParsedValue(id.ToStringValue(), id.Span);
public static TokenListParser LabelMatcher =
from id in LabelValueMatcher
from op in MatchOp
from str in StringLiteral
- select new LabelMatcher(id, op, (StringLiteral)str);
+ select new LabelMatcher(id.Value, op, (StringLiteral)str, id.Span.UntilEnd(str.Span));
public static TokenListParser MatchOp =
Token.EqualTo(PromToken.EQL).Select(_ => Operators.LabelMatch.Equal)
@@ -109,7 +111,7 @@ from str in StringLiteral
public static TokenListParser Number =
from s in (
Token.EqualTo(PromToken.ADD).Or(Token.EqualTo(PromToken.SUB))
- ).OptionalOrDefault(new Token(PromToken.ADD, TextSpan.Empty))
+ ).OptionalOrDefault(new Token(PromToken.ADD, TextSpan.None))
from n in Token.EqualTo(PromToken.NUMBER)
select new NumberLiteral(
(n.ToStringValue(), s.Kind) switch
@@ -117,7 +119,8 @@ from n in Token.EqualTo(PromToken.NUMBER)
(var v, PromToken.ADD) when v.Equals("Inf", StringComparison.OrdinalIgnoreCase) => double.PositiveInfinity,
(var v, PromToken.SUB) when v.Equals("Inf", StringComparison.OrdinalIgnoreCase) => double.NegativeInfinity,
(var v, var op) => double.Parse(v) * (op == PromToken.SUB ? -1.0 : 1.0)
- }
+ },
+ s.Span.Length > 0 ? s.Span.UntilEnd(n.Span) : n.Span
);
///
@@ -153,7 +156,7 @@ static TimeSpan ParseComponent(Match m, int index, Func parser)
ts += ParseComponent(match, 12, i => TimeSpan.FromSeconds(i));
ts += ParseComponent(match, 14, i => TimeSpan.FromMilliseconds(i));
- return new Duration(ts);
+ return new Duration(ts, n.Span);
});
// TODO support unicode, octal and hex escapes
@@ -197,43 +200,71 @@ from close in Character.EqualTo('`')
{
var c = t.Span.ConsumeChar();
if (c.Value == '\'')
- return new StringLiteral('\'', SingleQuoteStringLiteral.Parse(t.Span.ToStringValue()));
+ return new StringLiteral('\'', SingleQuoteStringLiteral.Parse(t.Span.ToStringValue()), t.Span);
if (c.Value == '"')
- return new StringLiteral('"', DoubleQuoteStringLiteral.Parse(t.Span.ToStringValue()));
+ return new StringLiteral('"', DoubleQuoteStringLiteral.Parse(t.Span.ToStringValue()), t.Span);
if (c.Value == '`')
- return new StringLiteral('`', RawString.Parse(t.Span.ToStringValue()));
+ return new StringLiteral('`', RawString.Parse(t.Span.ToStringValue()), t.Span);
throw new ParseException($"Unexpected string quote", t.Span.Position);
});
+ private static readonly HashSet ValidOffsetExpressions = new HashSet
+ {
+ typeof(MatrixSelector),
+ typeof(VectorSelector),
+ typeof(SubqueryExpr),
+ };
public static Func> OffsetExpr = (Expr expr) =>
+ (
from offset in Token.EqualTo(PromToken.OFFSET)
from neg in Token.EqualTo(PromToken.SUB).Optional()
from duration in Duration
- select new OffsetExpr(expr, new Duration(new TimeSpan(duration.Value.Ticks * (neg.HasValue ? -1 : 1))));
+ // Where needs to be called once the parser has definitely been advanced beyond the initial token (offset)
+ // it parses in order for Or() to consider this a partial failure
+ .Where(_ =>
+ ValidOffsetExpressions.Contains(expr.GetType()),
+ "offset modifier must be preceded by an instant vector selector or range vector selector or a subquery"
+ )
+ select new OffsetExpr(
+ expr,
+ new Duration(new TimeSpan(duration.Value.Ticks * (neg.HasValue ? -1 : 1))),
+ expr.Span!.Value.UntilEnd(duration.Span)
+ )
+ );
public static TokenListParser ParenExpression =
- from e in Parse.Ref(() => Expr).Between(Token.EqualTo(PromToken.LEFT_PAREN), Token.EqualTo(PromToken.RIGHT_PAREN))
- select new ParenExpression(e);
-
- public static TokenListParser FunctionArgs = Parse.Ref(() => Expr).ManyDelimitedBy(Token.EqualTo(PromToken.COMMA))
- .Between(Token.EqualTo(PromToken.LEFT_PAREN).Try(), Token.EqualTo(PromToken.RIGHT_PAREN));
+ from lp in Token.EqualTo(PromToken.LEFT_PAREN)
+ from e in Parse.Ref(() => Expr)
+ from rp in Token.EqualTo(PromToken.RIGHT_PAREN)
+ select new ParenExpression(e, lp.Span.UntilEnd(rp.Span));
+
+ public static TokenListParser> FunctionArgs =
+ from lp in Token.EqualTo(PromToken.LEFT_PAREN).Try()
+ from args in Parse.Ref(() => Expr).ManyDelimitedBy(Token.EqualTo(PromToken.COMMA))
+ from rp in Token.EqualTo(PromToken.RIGHT_PAREN)
+ select args.ToParsedValue(lp.Span, rp.Span);
public static TokenListParser FunctionCall =
- from id in Token.EqualTo(PromToken.IDENTIFIER).Where(x => Functions.Names.Contains(x.ToStringValue())).Try()
+ from id in Token.EqualTo(PromToken.IDENTIFIER).Where(x => Functions.Map.ContainsKey(x.ToStringValue())).Try()
+ let function = Functions.Map[id.ToStringValue()]
from args in FunctionArgs
- select new FunctionCall(id.ToStringValue(), args.ToImmutableArray());
+ .Where(a => function.IsVariadic || (!function.IsVariadic && function.ArgTypes.Length == a.Value.Length), $"Incorrect number of argument(s) in call to {function.Name}, expected {function.ArgTypes.Length} argument(s)")
+ .Where(a => !function.IsVariadic || (function.IsVariadic && a.Value.Length >= function.MinArgCount), $"Incorrect number of argument(s) in call to {function.Name}, expected at least {function.MinArgCount} argument(s)")
+ // TODO validate "at most" arguments- https://github.com/prometheus/prometheus/blob/7471208b5c8ff6b65b644adedf7eb964da3d50ae/promql/parser/parse.go#L552
+ select new FunctionCall(function, args.Value.ToImmutableArray(), id.Span.UntilEnd(args.Span));
- public static TokenListParser> GroupingLabels =
+ public static TokenListParser>> GroupingLabels =
+ from lParen in Token.EqualTo(PromToken.LEFT_PAREN)
from labels in (LabelValueMatcher.ManyDelimitedBy(Token.EqualTo(PromToken.COMMA)))
- .Between(Token.EqualTo(PromToken.LEFT_PAREN), Token.EqualTo(PromToken.RIGHT_PAREN))
- select labels.Select(x => x).ToImmutableArray();
+ from rParen in Token.EqualTo(PromToken.RIGHT_PAREN)
+ select labels.Select(x => x.Value).ToImmutableArray().ToParsedValue(lParen.Span, rParen.Span);
- public static TokenListParser BoolModifier =
+ public static TokenListParser> BoolModifier =
from b in Token.EqualTo(PromToken.BOOL).Optional()
- select b.HasValue;
+ select b.HasValue.ToParsedValue(b?.Span ?? TextSpan.None);
public static TokenListParser OnOrIgnoring =
from b in BoolModifier
@@ -241,10 +272,11 @@ from onOrIgnoring in Token.EqualTo(PromToken.ON).Or(Token.EqualTo(PromToken.IGNO
from onOrIgnoringLabels in GroupingLabels
select new VectorMatching(
Operators.VectorMatchCardinality.OneToOne,
- onOrIgnoringLabels,
+ onOrIgnoringLabels.Value,
onOrIgnoring.HasValue && onOrIgnoring.Kind == PromToken.ON,
ImmutableArray.Empty,
- b
+ b.Value,
+ b.HasSpan ? b.Span.UntilEnd(onOrIgnoringLabels.Span) : onOrIgnoring.Span.UntilEnd(onOrIgnoringLabels.Span)
);
public static Func> SubqueryExpr = (Expr expr) =>
@@ -253,13 +285,13 @@ from range in Duration
from colon in Token.EqualTo(PromToken.COLON)
from step in Duration.AsNullable().OptionalOrDefault()
from rb in Token.EqualTo(PromToken.RIGHT_BRACKET)
- select new SubqueryExpr(expr, range, step);
+ select new SubqueryExpr(expr, range, step, expr.Span!.Value.UntilEnd(rb.Span));
public static TokenListParser VectorMatching =
from vectMatching in (
from vm in OnOrIgnoring
from grp in Token.EqualTo(PromToken.GROUP_LEFT).Or(Token.EqualTo(PromToken.GROUP_RIGHT))
- from grpLabels in GroupingLabels.OptionalOrDefault(ImmutableArray.Empty)
+ from grpLabels in GroupingLabels.OptionalOrDefault(ImmutableArray.Empty.ToEmptyParsedValue())
select vm with
{
MatchCardinality = grp switch
@@ -269,14 +301,15 @@ select vm with
{Kind: PromToken.GROUP_RIGHT} => Operators.VectorMatchCardinality.OneToMany,
_ => Operators.VectorMatchCardinality.OneToOne
},
- Include = grpLabels
+ Include = grpLabels.Value,
+ Span = vm.Span!.Value.UntilEnd(grpLabels.HasSpan ? grpLabels.Span : grp.Span)
}
).Try().Or(
from vm in OnOrIgnoring
select vm
).Try().Or(
from b in BoolModifier
- select new VectorMatching(b)
+ select new VectorMatching(b.Value) { Span = b.Span }
)
select vectMatching;
@@ -301,32 +334,107 @@ from b in BoolModifier
};
public static TokenListParser BinaryExpr =
- from lhs in Parse.Ref(() => ExprNotBinary)
- from op in Token.Matching(x => BinaryOperatorMap.ContainsKey(x), "binary_op")
- from vm in VectorMatching.AsNullable().OptionalOrDefault()
- from rhs in Parse.Ref(() => Expr)
- select new BinaryExpr(lhs, rhs, BinaryOperatorMap[op.Kind], vm);
+ // Sprache doesn't support lef recursive grammars so we have to parse out binary expressions as lists of non-binary expressions
+ from head in Parse.Ref(() => ExprNotBinary)
+ from tail in (
+ from opToken in Token.Matching(x => BinaryOperatorMap.ContainsKey(x), "binary_op")
+ let op = BinaryOperatorMap[opToken.Kind]
+ from vm in VectorMatching.AsNullable().OptionalOrDefault()
+ .Where(x => x is not { ReturnBool: true } || (x.ReturnBool && Operators.BinaryComparisonOperators.Contains(op)), "bool modifier can only be used on comparison operators")
+ from expr in Parse.Ref(() => ExprNotBinary)
+ select (op, vm, expr)
+ ).AtLeastOnce()
+ select CreateBinaryExpression(head, tail);
+
+ ///
+ /// Creates a binary expression from a collection of two or more operands and one or more operators.
+ ///
+ ///
+ /// This function need to ensure operator precedence is maintained, e.g. 1 + 2 * 3 or 4 should parsed as (1 + (2 * 3)) or 4.
+ ///
+ /// The first operand
+ /// The trailing operators, vector matching + operands
+ private static BinaryExpr CreateBinaryExpression(Expr head, (Operators.Binary op, VectorMatching? vm, Expr expr)[] tail)
+ {
+ // Just two operands, no need to do any precedence checking
+ if (tail.Length == 1)
+ return new BinaryExpr(head, tail[0].expr, tail[0].op, tail[0].vm, head.Span!.Value.UntilEnd(tail[0].expr.Span));
+
+ // Three + operands and we need to group subexpressions by precedence. First things first: create linked lists of all our operands and operators
+ var operands = new LinkedList(new[] { head }.Concat(tail.Select(x => x.expr)));
+ var operators = new LinkedList<(Operators.Binary op, VectorMatching? vm)>(tail.Select(x => (x.op, x.vm)));
+
+ // Iterate through each level of operator precedence, moving from highest -> lowest
+ foreach (var precedenceLevel in Operators.BinaryPrecedence)
+ {
+ var lhs = operands.First;
+ var op = operators.First;
+
+ // While we have operators left to consume, iterate through each operand + operator
+ while (op != null)
+ {
+ var rhs = lhs!.Next!;
+
+ // This operator has the same precedence of the current precedence level- create a new binary subexpression with the current operands + operators
+ if (precedenceLevel.Contains(op.Value.op))
+ {
+ var b = new BinaryExpr(lhs.Value, rhs.Value, op.Value.op, op.Value.vm, lhs.Value.Span!.Value.UntilEnd(rhs.Value.Span)); // TODO span matching
+ var bNode = operands.AddBefore(rhs, b);
+
+ // Remove the previous operands (will replace with our new binary expression)
+ operands.Remove(lhs);
+ operands.Remove(rhs);
+
+ lhs = bNode;
+ var nextOp = op.Next;
+
+ // Remove the operator
+ operators.Remove(op);
+ op = nextOp;
+ }
+ else
+ {
+ // Move on to the next operand + operator
+ lhs = rhs;
+ op = op.Next;
+ }
+ }
+ }
+
+ return (BinaryExpr)operands.Single();
+ }
- public static TokenListParser labels)> AggregateModifier =
+ public static TokenListParser labels)>> AggregateModifier =
from kind in Token.EqualTo(PromToken.BY).Try()
.Or(Token.EqualTo(PromToken.WITHOUT).Try())
from labels in GroupingLabels
- select (kind.Kind == PromToken.WITHOUT, labels);
+ select (kind.Kind == PromToken.WITHOUT, labels.Value).ToParsedValue(kind.Span, labels.Span);
public static TokenListParser AggregateExpr =
- from op in Token.EqualTo(PromToken.AGGREGATE_OP).Where(x => Operators.Aggregates.Contains(x.Span.ToStringValue())).Try()
+ from op in Token.EqualTo(PromToken.AGGREGATE_OP)
+ .Where(x => Operators.Aggregates.ContainsKey(x.ToStringValue())).Try()
+ let aggOps = Operators.Aggregates[op.ToStringValue()]
from argsAndMod in (
from args in FunctionArgs
- from mod in AggregateModifier.OptionalOrDefault((without: false, labels: ImmutableArray.Empty))
- select (mod, args)
+ from mod in AggregateModifier.OptionalOrDefault(
+ (without: false, labels: ImmutableArray.Empty).ToEmptyParsedValue()
+ )
+ select (mod, args: args.Value).ToParsedValue(args.Span, mod.HasSpan ? mod.Span : args.Span)
).Or(
from mod in AggregateModifier
from args in FunctionArgs
- select (mod, args)
+ select (mod, args: args.Value).ToParsedValue(mod.Span, args.Span)
)
- .Where(x => x.args.Length >= 1, "At least one argument is required for aggregate expressions")
- .Where(x => x.args.Length <= 2, "A maximum of two arguments is supported for aggregate expressions")
- select new AggregateExpr(op.ToStringValue(), argsAndMod.args.Length > 1 ? argsAndMod.args[1] : argsAndMod.args[0], argsAndMod.args.Length > 1 ? argsAndMod.args[0] : null, argsAndMod.mod.labels, argsAndMod.mod.without );
+ .Where(x => aggOps.ParameterType == null || (aggOps.ParameterType != null && x.Value.args.Length == 2), "wrong number of arguments for aggregate expression provided, expected 2, got 1")
+ .Where(x => aggOps.ParameterType != null || (aggOps.ParameterType == null && x.Value.args.Length == 1), "wrong number of arguments for aggregate expression provided, expected 1, got 2")
+ select new AggregateExpr(
+ aggOps,
+ argsAndMod.Value.args.Length > 1 ? argsAndMod.Value.args[1] : argsAndMod.Value.args[0],
+ argsAndMod.Value.args.Length > 1 ? argsAndMod.Value.args[0] : null,
+ argsAndMod.Value.mod.Value.labels,
+ argsAndMod.Value.mod.Value.without,
+ Span: op.Span.UntilEnd(argsAndMod.Span)
+ );
public static TokenListParser ExprNotBinary =
from head in OneOf(
@@ -341,23 +449,26 @@ from head in OneOf(
Parse.Ref(() => StringLiteral).Cast()
)
#pragma warning disable CS8602
- from offsetOrSubquery in Parse.Ref(() => OffsetOrSubquery(head)).AsNullable().OptionalOrDefault()
+ from offsetOrSubquery in Parse.Ref(() => OffsetOrSubquery(head))
#pragma warning restore CS8602
select offsetOrSubquery == null ? head : offsetOrSubquery;
- public static Func> OffsetOrSubquery = (Expr expr) =>
- from offsetOfSubquery in (
- from offset in OffsetExpr(expr)
- select (Expr)offset
- ).Or(
- from subquery in SubqueryExpr(expr)
- select (Expr)subquery
- )
- select offsetOfSubquery;
+ public static Func> OffsetOrSubquery = (Expr expr) =>
+ (
+ from offset in OffsetExpr(expr)
+ select (Expr) offset
+ ).Or(
+ from subquery in SubqueryExpr(expr)
+ select (Expr) subquery
+ )
+ .AsNullable()
+ .Or(
+ Parse.Return(null)
+ );
public static TokenListParser Expr { get; } =
from head in Parse.Ref(() => BinaryExpr).Cast().Try().Or(ExprNotBinary)
- from offsetOrSubquery in OffsetOrSubquery(head).AsNullable().OptionalOrDefault()
+ from offsetOrSubquery in OffsetOrSubquery(head)
select offsetOrSubquery == null ? head : offsetOrSubquery;
///
@@ -386,4 +497,46 @@ private static TokenListParser OneOf(params TokenListParser
+ {
+ public ParsedValue(T value, TextSpan span)
+ {
+ Value = value;
+ Span = span;
+ }
+
+ public T Value { get; }
+ public bool HasSpan => Span != TextSpan.None;
+ public TextSpan Span { get; }
+ }
+
+ public static class Extensions
+ {
+ public static ParsedValue ToParsedValue(this T result, TextSpan start, TextSpan end)
+ {
+ return new ParsedValue(result, start.UntilEnd(end));
+ }
+
+ public static ParsedValue ToEmptyParsedValue(this T result)
+ {
+ return new ParsedValue(result, TextSpan.None);
+ }
+
+ public static TextSpan UntilEnd(this TextSpan @base, TextSpan? next)
+ {
+ if (next == null)
+ return @base;
+
+ int absolute1 = next.Value.Position.Absolute + next.Value.Length;
+ int absolute2 = @base.Position.Absolute;
+ return @base.First(absolute1 - absolute2);
+ }
+
+ public static ParsedValue ToParsedValue(this T result, TextSpan span)
+ {
+ return new ParsedValue(result, span);
+ }
+
+ }
}
diff --git a/src/PromQL.Parser/Printer.cs b/src/PromQL.Parser/Printer.cs
index 83fa20a..1e3d4ca 100644
--- a/src/PromQL.Parser/Printer.cs
+++ b/src/PromQL.Parser/Printer.cs
@@ -4,6 +4,8 @@
namespace PromQL.Parser
{
+ // TODO fix bug around "and"
+ // TODO pretty print support
public class Printer : IVisitor
{
private StringBuilder _sb = new ();
@@ -132,7 +134,7 @@ public virtual void Visit(ParenExpression paren)
public virtual void Visit(FunctionCall fnCall)
{
- _sb.Append($"{fnCall.Identifier}(");
+ _sb.Append($"{fnCall.Function.Name}(");
bool isFirst = true;
foreach (var arg in fnCall.Args)
@@ -199,7 +201,7 @@ public virtual void Visit(BinaryExpr expr)
public virtual void Visit(AggregateExpr expr)
{
- _sb.Append($"{expr.OperatorName}");
+ _sb.Append($"{expr.Operator.Name}");
if (expr.GroupingLabels.Length > 0)
{
diff --git a/src/PromQL.Parser/Tokenizer.cs b/src/PromQL.Parser/Tokenizer.cs
index 6d1cdc5..d3c1af6 100644
--- a/src/PromQL.Parser/Tokenizer.cs
+++ b/src/PromQL.Parser/Tokenizer.cs
@@ -87,7 +87,7 @@ from close in Character.EqualTo(quoteChar)
.Select(x =>
{
var idOrKeyword = x.ToStringValue();
- if (Operators.Aggregates.Contains(idOrKeyword))
+ if (Operators.Aggregates.ContainsKey(idOrKeyword))
return PromToken.AGGREGATE_OP;
if (KeywordsToTokens.TryGetValue(idOrKeyword, out var keyToken))
return keyToken;
diff --git a/src/PromQL.Parser/TypeChecker.cs b/src/PromQL.Parser/TypeChecker.cs
new file mode 100644
index 0000000..22b30d7
--- /dev/null
+++ b/src/PromQL.Parser/TypeChecker.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Linq;
+using ExhaustiveMatching;
+using PromQL.Parser.Ast;
+using Superpower.Model;
+
+namespace PromQL.Parser
+{
+ public static class TypeChecker
+ {
+ public static ValueType CheckType(this Expr expr)
+ {
+ switch (expr)
+ {
+ case AggregateExpr aggExpr:
+ // TODO Currently don't check for parameter counts here
+ // The parser does this currently but we might want to extend the logic for user generated ASTs.
+ if (aggExpr.Operator.ParameterType != null)
+ ExpectTypes(aggExpr.Param!, aggExpr.Operator.ParameterType.Value);
+
+ ExpectTypes(aggExpr.Expr, ValueType.Vector);
+
+ return aggExpr.Type;
+
+ case BinaryExpr binExpr:
+ var lhsType = ExpectTypes(binExpr.LeftHandSide, ValueType.Scalar, ValueType.Vector);
+ var rhsType = ExpectTypes(binExpr.RightHandSide, ValueType.Scalar, ValueType.Vector);
+
+ if (Operators.BinaryComparisonOperators.Contains(binExpr.Operator) && !(binExpr.VectorMatching?.ReturnBool ?? false)
+ && lhsType == ValueType.Scalar && rhsType == ValueType.Scalar)
+ {
+ throw new InvalidTypeException("comparisons between scalars must use bool modifier", binExpr.Span);
+ }
+
+
+ // TODO https://github.com/prometheus/prometheus/blob/7471208b5c8ff6b65b644adedf7eb964da3d50ae/promql/parser/parse.go#L526-L534
+
+ if ((lhsType == ValueType.Scalar || rhsType == ValueType.Scalar) && Operators.BinarySetOperators.Contains(binExpr.Operator))
+ throw new InvalidTypeException($"set operator {binExpr.Operator} not allowed in binary scalar expression", binExpr.Span);
+
+ return binExpr.Type;
+
+ case FunctionCall fnCall:
+ var expectedArgTypes = fnCall.Function.ArgTypes;
+
+ // Varadic functions can repeat the last parameter type indefinitely
+ if (fnCall.Function.IsVariadic)
+ expectedArgTypes = expectedArgTypes.AddRange(
+ Enumerable.Repeat(
+ fnCall.Function.ArgTypes.Last(),
+ fnCall.Args.Length - fnCall.Function.MinArgCount
+ )
+ );
+
+ foreach (var (arg, expectedType) in fnCall.Args.Zip(expectedArgTypes))
+ ExpectTypes(arg, expectedType);
+
+ return fnCall.Type;
+
+ case SubqueryExpr subqueryExpr:
+ ExpectTypes(subqueryExpr.Expr, ValueType.Vector);
+ return subqueryExpr.Type;
+
+ case UnaryExpr unaryExpr:
+ ExpectTypes(unaryExpr.Expr, ValueType.Scalar, ValueType.Vector);
+ return unaryExpr.Type;
+
+ case OffsetExpr offsetExpr:
+ return offsetExpr.Expr.CheckType();
+
+ case ParenExpression parenExpr:
+ return parenExpr.Expr.CheckType();
+
+ case NumberLiteral _:
+ case StringLiteral _:
+ case MatrixSelector _:
+ case VectorSelector _:
+ return expr.Type;
+
+ default:
+ throw ExhaustiveMatch.Failed(expr);
+ }
+ }
+
+ private static ValueType ExpectTypes(Expr expr, params ValueType[] expected)
+ {
+ var type = expr.CheckType();
+ if (!expected.Contains(type))
+ throw new InvalidTypeException(expected, expr.Type, expr.Span);
+
+ return type;
+ }
+
+ public class InvalidTypeException : Exception
+ {
+ public InvalidTypeException(ValueType[] expected, ValueType provided, TextSpan? span)
+ : this($"Unexpected type '{AsHumanReadable(provided)}' was provided, expected {(expected.Length == 1 ? AsHumanReadable(expected[0]): $"one of {string.Join(", ", expected.Select(e => $"'{AsHumanReadable(e)}'"))}")}", span)
+ {
+ }
+
+ public InvalidTypeException(string message, TextSpan? span)
+ : base($"{message}{(span.HasValue ? $": {span.Value.Position.ToString()}" : "")}")
+ {
+ }
+
+ private static string AsHumanReadable(ValueType vt) => vt switch
+ {
+ ValueType.Scalar => "scalar",
+ ValueType.Matrix => "range vector",
+ ValueType.Vector => "instant vector",
+ ValueType.String => "string",
+ ValueType.None => "none",
+ _ => throw ExhaustiveMatch.Failed(vt)
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PromQL.Parser/ValueType.cs b/src/PromQL.Parser/ValueType.cs
new file mode 100644
index 0000000..e5474d8
--- /dev/null
+++ b/src/PromQL.Parser/ValueType.cs
@@ -0,0 +1,14 @@
+namespace PromQL.Parser
+{
+ ///
+ /// Taken from https://github.com/prometheus/prometheus/blob/277bf93952b56227cb750a8129197efa489eddde/promql/parser/value.go#L26-L32.
+ ///
+ public enum ValueType
+ {
+ None,
+ Scalar,
+ Vector,
+ Matrix,
+ String,
+ }
+}
\ No newline at end of file
diff --git a/tests/PromQL.Parser.Tests/ParserTests.cs b/tests/PromQL.Parser.Tests/ParserTests.cs
index d717229..7a1c422 100644
--- a/tests/PromQL.Parser.Tests/ParserTests.cs
+++ b/tests/PromQL.Parser.Tests/ParserTests.cs
@@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
-using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using PromQL.Parser.Ast;
using Superpower;
-using Superpower.Display;
using Superpower.Model;
namespace PromQL.Parser.Tests
@@ -25,11 +23,11 @@ public void SetUp()
[Test]
public void StringLiteralDoubleQuote() => Parse(Parser.StringLiteral, "\"A string\"")
- .Should().Be(new StringLiteral('"', "A string"));
+ .Should().Be(new StringLiteral('"', "A string", new TextSpan("\"A string\"")));
[Test]
public void StringLiteralDoubleQuoteEscaped() => Parse(Parser.StringLiteral, @"""\a\b\f\n\r\t\v\\\""""")
- .Should().Be(new StringLiteral('"', "\a\b\f\n\r\t\v\\\""));
+ .Should().Be(new StringLiteral('"', "\a\b\f\n\r\t\v\\\"", new TextSpan(@"""\a\b\f\n\r\t\v\\\""""")));
[Test]
public void StringLiteralDoubleQuoteNewline() =>
@@ -37,11 +35,11 @@ public void StringLiteralDoubleQuoteNewline() =>
[Test]
public void StringLiteralSingleQuote() => Parse(Parser.StringLiteral, "'A string'")
- .Should().Be(new StringLiteral('\'', "A string"));
+ .Should().Be(new StringLiteral('\'', "A string", new TextSpan("'A string'")));
[Test]
public void StringLiteralSingleQuoteEscaped() => Parse(Parser.StringLiteral, @"'\a\b\f\n\r\t\v\\\''")
- .Should().Be(new StringLiteral('\'', "\a\b\f\n\r\t\v\\\'"));
+ .Should().Be(new StringLiteral('\'', "\a\b\f\n\r\t\v\\\'", new TextSpan(@"'\a\b\f\n\r\t\v\\\''")));
[Test]
public void StringLiteralSingleQuoteNewline() =>
@@ -49,15 +47,15 @@ public void StringLiteralSingleQuoteNewline() =>
[Test]
public void StringLiteralRaw() => Parse(Parser.StringLiteral, "`A string`")
- .Should().Be(new StringLiteral('`', "A string"));
+ .Should().Be(new StringLiteral('`', "A string", new TextSpan("`A string`")));
[Test]
public void StringLiteralRaw_Multiline() => Parse(Parser.StringLiteral, "`A\n string`")
- .Should().Be(new StringLiteral('`', "A\n string"));
+ .Should().Be(new StringLiteral('`', "A\n string", new TextSpan("`A\n string`")));
[Test]
public void StringLiteralRaw_NoEscapes() => Parse(Parser.StringLiteral, @"`\a\b\f\n\r\t\v`")
- .Should().Be(new StringLiteral('`', @"\a\b\f\n\r\t\v"));
+ .Should().Be(new StringLiteral('`', @"\a\b\f\n\r\t\v", new TextSpan(@"`\a\b\f\n\r\t\v`")));
[Test]
[TestCase("2y52w365d25h10m30s100ms", "1460.01:10:30.100")]
@@ -67,7 +65,7 @@ public void StringLiteralRaw_NoEscapes() => Parse(Parser.StringLiteral, @"`\a\b\
[TestCase("180s", "00:03:00")]
[TestCase("500ms", "00:00:00.500")]
public void Duration(string input, string expected) => Parse(Parser.Duration, input)
- .Should().Be(new Duration(TimeSpan.Parse(expected)));
+ .Should().Be(new Duration(TimeSpan.Parse(expected), new TextSpan(input)));
[Test]
@@ -80,37 +78,38 @@ public void Duration(string input, string expected) => Parse(Parser.Duration, in
[TestCase("1e5", 100000)]
[TestCase("1.55E+5", 155000)]
public void Number(string input, double expected) => Parse(Parser.Number, input)
- .Should().Be(new NumberLiteral(expected));
+ .Should().Be(new NumberLiteral(expected, new TextSpan(input)));
[Test]
[TestCase("nan")]
[TestCase("NaN")]
public void Number_NaN(string input) => Parse(Parser.Number, input)
- .Should().Be(new NumberLiteral(double.NaN));
+ .Should().Be(new NumberLiteral(double.NaN, new TextSpan(input)));
[Test]
[TestCase("Inf")]
[TestCase("+inf")]
public void Number_InfPos(string input) => Parse(Parser.Number, input)
- .Should().Be(new NumberLiteral(double.PositiveInfinity));
+ .Should().Be(new NumberLiteral(double.PositiveInfinity, new TextSpan(input)));
[Test]
[TestCase("-Inf")]
[TestCase("-inf")]
public void Number_InfNeg(string input) => Parse(Parser.Number, input)
- .Should().Be(new NumberLiteral(double.NegativeInfinity));
+ .Should().Be(new NumberLiteral(double.NegativeInfinity, new TextSpan(input)));
[Test]
[TestCaseSource(nameof(FunctionsAggregatesAndOperators))]
public void LabelValueMatcher_FunctionsOperatorsAndKeywords(string identifier) =>
- Parse(Parser.LabelValueMatcher, identifier).Should().Be(identifier);
+ Parse(Parser.LabelValueMatcher, identifier).Value.Should().Be(identifier);
[Test]
public void LabelMatchers_Empty()
{
- Parse(Parser.LabelMatchers, "{}")
- .Should().Be(new LabelMatchers(ImmutableArray.Empty));
+ const string input = "{}";
+ Parse(Parser.LabelMatchers, input)
+ .Should().Be(new LabelMatchers(ImmutableArray.Empty, new TextSpan(input)));
}
[Test]
@@ -122,86 +121,143 @@ public void LabelMatchers_EmptyNoComma()
[Test]
public void LabelMatchers_One()
{
- Parse(Parser.LabelMatchers, "{blah=\"my_label\"}")
- .Should().BeEquivalentTo(new LabelMatchers(new []
- {
- new LabelMatcher("blah", Operators.LabelMatch.Equal, new StringLiteral('"', "my_label"))
- }.ToImmutableArray()));
+ const string input = "{blah=\"my_label\"}";
+ Parse(Parser.LabelMatchers, input)
+ .Should().BeEquivalentTo(
+ new LabelMatchers(new []
+ {
+ new LabelMatcher(
+ "blah",
+ Operators.LabelMatch.Equal,
+ new StringLiteral('"', "my_label", new TextSpan(input, new Position(6, 0, 0), 10)),
+ new TextSpan(input, new Position(1, 0, 0), 15)
+ )
+ }.ToImmutableArray(),
+ Span: new TextSpan(input)
+ )
+ );
}
[Test]
public void LabelMatchers_OneTrailingComma()
{
- Parse(Parser.LabelMatchers, "{blah=\"my_label\" , }")
- .Should().BeEquivalentTo(new LabelMatchers(new[]
- {
- new LabelMatcher("blah", Operators.LabelMatch.Equal, new StringLiteral('"', "my_label"))
- }.ToImmutableArray()));
+ const string input = "{blah=\"my_label\" , }";
+ Parse(Parser.LabelMatchers, input)
+ .Should().BeEquivalentTo(
+ new LabelMatchers(
+ new[]
+ {
+ new LabelMatcher(
+ "blah",
+ Operators.LabelMatch.Equal,
+ new StringLiteral('"', "my_label", new TextSpan(input, new Position(6, 0, 0), 10)),
+ new TextSpan(input, new Position(1, 0, 0), 15)
+ ),
+ }.ToImmutableArray(),
+ new TextSpan(input)
+ )
+ );
}
[Test]
public void LabelMatchers_Many()
{
Parse(Parser.LabelMatchers, "{ blah=\"my_label\", blah_123 != 'my_label', b123=~'label', b_!~'label' }")
- .Should().BeEquivalentTo(new LabelMatchers(new []
- {
- new LabelMatcher("blah", Operators.LabelMatch.Equal, new StringLiteral('"', "my_label")),
- new LabelMatcher("blah_123", Operators.LabelMatch.NotEqual, new StringLiteral('\'', "my_label")),
- new LabelMatcher("b123", Operators.LabelMatch.Regexp, new StringLiteral('\'', "label")),
- new LabelMatcher("b_", Operators.LabelMatch.NotRegexp, new StringLiteral('\'', "label"))
- }.ToImmutableArray()));
+ .Should().BeEquivalentTo(
+ new LabelMatchers(
+ new []
+ {
+ new LabelMatcher("blah", Operators.LabelMatch.Equal, new StringLiteral('"', "my_label")),
+ new LabelMatcher("blah_123", Operators.LabelMatch.NotEqual, new StringLiteral('\'', "my_label")),
+ new LabelMatcher("b123", Operators.LabelMatch.Regexp, new StringLiteral('\'', "label")),
+ new LabelMatcher("b_", Operators.LabelMatch.NotRegexp, new StringLiteral('\'', "label"))
+ }.ToImmutableArray()
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
+ );
}
[Test]
public void VectorSelector_MetricIdentifier()
{
- Parse(Parser.VectorSelector, ":this_is_a_metric")
- .Should().BeEquivalentTo(new VectorSelector(new MetricIdentifier(":this_is_a_metric")));
+ const string input = ":this_is_a_metric";
+ Parse(Parser.VectorSelector, input)
+ .Should().BeEquivalentTo(
+ new VectorSelector(new MetricIdentifier(input, new TextSpan(input)), new TextSpan(input))
+ );
}
[Test]
public void VectorSelector_MetricIdentifier_And_LabelMatchers()
{
- Parse(Parser.VectorSelector, ":this_is_a_metric { }")
- .Should().BeEquivalentTo(new VectorSelector(new MetricIdentifier(":this_is_a_metric"), new LabelMatchers(ImmutableArray.Empty)));
+ const string input = ":this_is_a_metric { }";
+ Parse(Parser.VectorSelector, input)
+ .Should().BeEquivalentTo(
+ new VectorSelector(
+ new MetricIdentifier(":this_is_a_metric", new TextSpan(input, new Position(0, 0, 0), 17)),
+ new LabelMatchers(ImmutableArray.Empty, new TextSpan(input, new Position(18, 0, 0), 3)),
+ new TextSpan(input)
+ )
+ );
}
[Test]
public void VectorSelector_LabelMatchers()
{
- Parse(Parser.VectorSelector, "{ }")
- .Should().BeEquivalentTo(new VectorSelector(new LabelMatchers(ImmutableArray.Empty)));
+ const string input = "{ }";
+ Parse(Parser.VectorSelector, input)
+ .Should().BeEquivalentTo(
+ new VectorSelector(
+ new LabelMatchers(ImmutableArray.Empty, new TextSpan(input)),
+ new TextSpan(input)
+ )
+ );
}
[Test]
- public void MatrixSelector() => Parse(Parser.MatrixSelector, " metric { } [ 1m1s ] ")
- .Should().Be(new MatrixSelector(
- new VectorSelector(new MetricIdentifier("metric"), new LabelMatchers(ImmutableArray.Empty)),
- new Duration(new TimeSpan(0, 1, 1))
- ));
+ public void MatrixSelector()
+ {
+ var input = "metric { } [ 1m1s ]";
+ Parse(Parser.MatrixSelector, input)
+ .Should().Be(new MatrixSelector(
+ new VectorSelector(
+ new MetricIdentifier("metric", new TextSpan(input, Position.Zero, 6)),
+ new LabelMatchers(ImmutableArray.Empty, new TextSpan(input, new Position(7, 0, 0), 3)),
+ new TextSpan(input, Position.Zero, 10)
+ ),
+ new Duration(new TimeSpan(0, 1, 1), new TextSpan(input, new Position(13, 0, 0), 4)),
+ new TextSpan(input)
+ ));
+ }
[Test]
public void Offset_Vector()
{
- var expr = Parse(Parser.Expr, " metric { } offset 1m");
+ const string input = "metric { } offset 1m";
+ var expr = Parse(Parser.Expr, input);
var offsetExpr = expr.Should().BeOfType().Subject;
offsetExpr.Expr.Should().BeOfType();
offsetExpr.Duration.Value.Should().Be(TimeSpan.FromMinutes(1));
+ offsetExpr.Span.Should().Be(new TextSpan(input));
}
[Test]
public void Offset_MatrixSelector()
{
- var expr = Parse(Parser.Expr, " metric { } [ 1m1s ] offset -7m");
+ const string input = "metric { } [ 1m1s ] offset -7m";
+ var expr = Parse(Parser.Expr, input);
var offsetExpr = expr.Should().BeOfType().Subject;
offsetExpr.Expr.Should().BeOfType();
offsetExpr.Duration.Value.Should().Be(TimeSpan.FromMinutes(-7));
+ offsetExpr.Span.Should().Be(new TextSpan(input));
}
[Test]
public void Offset_Subquery()
{
- var expr = Parse(Parser.Expr, " metric[ 1h:1m ] offset 1w");
+ var input = "metric[ 1h:1m ] offset 1w";
+ var expr = Parse(Parser.Expr, input);
expr.Should().BeEquivalentTo(
new OffsetExpr(
new SubqueryExpr(
@@ -210,14 +266,29 @@ public void Offset_Subquery()
new Duration(TimeSpan.FromMinutes(1))
),
new Duration(TimeSpan.FromDays(7))
- )
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
);
+ expr.Span.Should().Be(new TextSpan(input));
}
+ [Test]
+ [TestCase("time() offset 1m")]
+ [TestCase("avg(blah) offset 1m")]
+ [TestCase("'asdas' offset 1m")]
+ [TestCase("1 offset 1m")]
+ public void Offset_Invalid(string badQuery)
+ {
+ Assert.Throws(() => Parse(Parser.Expr, badQuery))
+ .Message.Should().Contain("offset modifier must be preceded by an instant vector selector or range vector selector or a subquery");
+ }
+
[Test]
public void Subquery_Offset()
{
- var expr = Parse(Parser.Expr, " metric offset 1w [ 1h:1m ]");
+ const string input = "metric offset 1w [ 1h:1m ]";
+ var expr = Parse(Parser.Expr, input);
expr.Should().BeEquivalentTo(
new SubqueryExpr(
new OffsetExpr(
@@ -226,8 +297,11 @@ public void Subquery_Offset()
),
new Duration(TimeSpan.FromHours(1)),
new Duration(TimeSpan.FromMinutes(1))
- )
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
);
+ expr.Span.Should().Be(new TextSpan(input));
}
[Test]
@@ -246,59 +320,133 @@ public void ParenExpr_MissingLeft()
public void ParenExpr_MissingRight()
{
Assert.Throws(() => Parse(Parser.Expr, " (( 1 ) "));
- }
-
- [Test]
- public void ParenExpr_Simple() => Parse(Parser.Expr, " (1) ")
- .Should().Be(new ParenExpression(new NumberLiteral(1.0)));
-
+ }
+
[Test]
- public void ParenExpr_Nested() => Parse(Parser.Expr, " ((1)) ")
- .Should().Be(new ParenExpression(new ParenExpression(new NumberLiteral(1.0))));
+ public void ParenExpr_Simple()
+ {
+ var input = " (1) ";
+ var parsed = Parse(Parser.Expr, input)
+ .Should().Be(
+ new ParenExpression(
+ new NumberLiteral(1.0, new TextSpan(input, new Position(2, 0, 0), 1)),
+ new TextSpan(input, new Position(1, 0, 0), 3)
+ )
+ );
+ }
[Test]
- public void FunctionCall_Empty() => Parse(Parser.FunctionCall, "time ()")
- .Should().Be(new FunctionCall("time", ImmutableArray.Empty));
+ public void ParenExpr_Nested()
+ {
+ var input = "((1))";
+ var parsed = Parse(Parser.Expr, input)
+ .Should().Be(
+ new ParenExpression(
+ new ParenExpression(
+ new NumberLiteral(1.0, new TextSpan(input, new Position(2, 0, 0), 1)),
+ new TextSpan(input, new Position(1, 0, 0), 3)
+ ),
+ new TextSpan(input)
+ )
+ );
+ }
[Test]
- public void FunctionCall_InvalidFunction()
+ public void FunctionCall_Empty()
+ {
+ var input = "time ()";
+ Parse(Parser.FunctionCall, input)
+ .Should().Be(new FunctionCall(Functions.Map["time"], ImmutableArray.Empty, new TextSpan(input)));
+ }
+
+ [Test]
+ public void FunctionCall_InvalidName()
{
Assert.Throws(() => Parse(Parser.Expr, "this_doesnt_exist ()"));
}
+
+ [Test]
+ public void FunctionCall_InvalidParameterCount()
+ {
+ Assert.Throws(() => Parse(Parser.Expr, "abs(-1, 1)"))
+ .Message.Contains("expected 1 argument(s)");
+ }
+
+ [Test]
+ public void FunctionCall_InvalidParameterCountVaradic()
+ {
+ Assert.Throws(() => Parse(Parser.Expr, "label_join(instant_vector, 'dst_label', 'separator')"))
+ .Message.Should().Contain("expected at least 4 argument(s)");
+ }
+
+ [Test]
+ [TestCase("hour()")]
+ [TestCase("hour(one)")]
+ [TestCase("hour(one, two)")]
+ [TestCase("label_join(instant_vector, 'dst_label', 'separator', 'one', 'two')")]
+ public void FunctionCall_Varadic(string query)
+ {
+ Parse(Parser.Expr, query).Should().BeOfType();
+ }
[Test]
public void FunctionCall_OneArg() => Parse(Parser.Expr, "abs (1)")
.Should().BeEquivalentTo(
- new FunctionCall("abs", new Expr[] { new NumberLiteral(1.0) }.ToImmutableArray())
+ new FunctionCall(Functions.Map["abs"], new Expr[] { new NumberLiteral(1.0) }.ToImmutableArray()),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
);
[Test]
- // NOTE: we do not either validate the parameter count or types of functions
- public void FunctionCall_MultiArg() => Parse(Parser.Expr, "abs (1, 2)")
+ // NOTE: we do not validate the types of functions in the parser
+ public void FunctionCall_MultiArg() => Parse(Parser.Expr, "histogram_quantile (0.9, blah)")
.Should().BeEquivalentTo(
- new FunctionCall("abs", new Expr[] { new NumberLiteral(1.0), new NumberLiteral(2.0) }.ToImmutableArray())
+ new FunctionCall(Functions.Map["histogram_quantile"], new Expr[] { new NumberLiteral(0.9), new VectorSelector(new MetricIdentifier("blah")) }.ToImmutableArray()),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
);
[Test]
- // NOTE: we do not either validate the parameter count or types of functions
public void FunctionCall_SnakeCase() => Parse(Parser.Expr, "absent_over_time (metric_name )")
.Should().BeEquivalentTo(
- new FunctionCall("absent_over_time", new Expr[] { new VectorSelector(new MetricIdentifier("metric_name")) }.ToImmutableArray())
+ new FunctionCall(Functions.Map["absent_over_time"], new Expr[] { new VectorSelector(new MetricIdentifier("metric_name")) }.ToImmutableArray()),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
);
[Test]
- public void UnaryExpr_Plus() => Parse(Parser.UnaryExpr, "+1")
- .Should().Be(new UnaryExpr(Operators.Unary.Add, new NumberLiteral(1.0)));
+ public void UnaryExpr_Plus()
+ {
+ const string input = "+1";
+ Parse(Parser.UnaryExpr, input)
+ .Should().Be(
+ new UnaryExpr(
+ Operators.Unary.Add,
+ new NumberLiteral(1.0, new TextSpan(input, new Position(1, 0 , 0), 1)),
+ new TextSpan(input)
+ )
+ );
+ }
[Test]
- public void UnaryExpr_Minus() => Parse(Parser.UnaryExpr, "-1")
- .Should().Be(new UnaryExpr(Operators.Unary.Sub, new NumberLiteral(1.0)));
-
+ public void UnaryExpr_Minus()
+ {
+ const string input = "-1";
+ Parse(Parser.UnaryExpr, input)
+ .Should().Be(
+ new UnaryExpr(
+ Operators.Unary.Sub,
+ new NumberLiteral(1.0, new TextSpan(input, new Position(1, 0 , 0), 1)),
+ new TextSpan(input)
+ )
+ );
+ }
+
[Test]
public void Expr()
{
- Parse(Parser.Expr, "'a string'").Should().Be(new StringLiteral('\'', "a string"));
- Parse(Parser.Expr, "1").Should().Be(new NumberLiteral(1.0));
+ Parse(Parser.Expr, "'a string'").Should().BeOfType();
+ Parse(Parser.Expr, "1").Should().BeOfType();
Parse(Parser.Expr, "1 + 1").Should().BeOfType();
Parse(Parser.Expr, "sum (some_expr)").Should().BeOfType();
Parse(Parser.Expr, "some_expr[1d:]").Should().BeOfType();
@@ -306,15 +454,15 @@ public void Expr()
Parse(Parser.Expr, "some_expr").Should().BeOfType();
Parse(Parser.Expr, "some_expr[1d]").Should().BeOfType();
Parse(Parser.Expr, "+(1)").Should().BeOfType();
- Parse(Parser.Expr, "abs()").Should().BeOfType();
+ Parse(Parser.Expr, "abs(-1)").Should().BeOfType();
}
[Test]
public void Expr_NumberWithSign()
{
// Make sure we don't parse this as a unary expression!
- Parse(Parser.Expr, "+1").Should().Be(new NumberLiteral(1));
- Parse(Parser.Expr, "-1").Should().Be(new NumberLiteral(-1));
+ Parse(Parser.Expr, "+1").Should().Be(new NumberLiteral(1, new TextSpan("+1")));
+ Parse(Parser.Expr, "-1").Should().Be(new NumberLiteral(-1, new TextSpan("-1")));
}
[Test]
@@ -338,7 +486,9 @@ public void Expr_NameKeywordWorkaround()
new VectorSelector(new LabelMatchers(new []
{
new LabelMatcher("__name__", Operators.LabelMatch.Equal, new StringLiteral('\'', "on"))
- }.ToImmutableArray()))
+ }.ToImmutableArray())),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
);
}
@@ -352,9 +502,9 @@ public void Expr_Complex()
Parse(Parser.Expr, toParse).Should().BeEquivalentTo(
new BinaryExpr(
new AggregateExpr(
- "sum",
+ Operators.Aggregates["sum"],
new FunctionCall(
- "rate",
+ Functions.Map["rate"],
new Expr[]{
new MatrixSelector(
new VectorSelector(new MetricIdentifier("node_cpu_seconds_total")),
@@ -367,9 +517,9 @@ public void Expr_Complex()
false
),
new AggregateExpr(
- "sum",
+ Operators.Aggregates["sum"],
new FunctionCall(
- "rate",
+ Functions.Map["rate"],
new Expr[]{
new MatrixSelector(
new VectorSelector(new MetricIdentifier("node_cpu_seconds_total")),
@@ -389,7 +539,9 @@ public void Expr_Complex()
ImmutableArray.Empty,
false
)
- )
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
);
}
@@ -412,7 +564,7 @@ public void Invalid_DurationExpr()
[Test]
public void ParseExpression_With_Comments()
{
- Parser.ParseExpression(" #some comment \n 1 \n # another comment!").Should().Be(new NumberLiteral(1.0));
+ Parser.ParseExpression(" #some comment \n 1 \n # another comment!").Should().BeOfType();
}
[Test]
@@ -423,85 +575,131 @@ public void GroupingLabels_Nothing()
[Test]
public void GroupingLabels_Empty() => Parse(Parser.GroupingLabels, " ( )")
- .Should().BeEmpty();
+ .Value.Should().BeEmpty();
[Test]
public void GroupingLabels_One() => Parse(Parser.GroupingLabels, " ( one ) ")
- .Should().Equal("one");
-
+ .Value.Should().Equal("one");
+
[Test]
- public void GroupingLabels_Many() => Parse(Parser.GroupingLabels, " ( one, two, three ) ")
- .Should().Equal("one", "two", "three");
+ public void GroupingLabels_Many()
+ {
+ var parsed = Parse(Parser.GroupingLabels, " ( one, two, three ) ");
+ parsed.Value.Should().Equal("one", "two", "three");
+ parsed.Span.ToString().Should().Be("( one, two, three )");
+ }
[Test]
- public void VectorMatching_Bool() => Parse(Parser.VectorMatching, "bool")
- .Should().BeEquivalentTo(new VectorMatching(
- Operators.VectorMatchCardinality.OneToOne,
- ImmutableArray.Empty,
- false,
- ImmutableArray.Empty,
- true)
- );
-
+ public void VectorMatching_Bool()
+ {
+ const string input = "bool";
+ Parse(Parser.VectorMatching, input)
+ .Should().BeEquivalentTo(new VectorMatching(
+ Operators.VectorMatchCardinality.OneToOne,
+ ImmutableArray.Empty,
+ false,
+ ImmutableArray.Empty,
+ true,
+ new TextSpan(input))
+ );
+ }
+
[Test]
- public void VectorMatching_Ignoring() => Parse(Parser.VectorMatching, "ignoring (one, two)")
- .Should().BeEquivalentTo(new VectorMatching(
- Operators.VectorMatchCardinality.OneToOne,
- new [] { "one", "two" }.ToImmutableArray(),
- false,
- ImmutableArray.Empty,
- false)
- );
+ public void VectorMatching_Ignoring()
+ {
+ const string input = "ignoring (one, two)";
+
+ Parse(Parser.VectorMatching, input)
+ .Should().BeEquivalentTo(new VectorMatching(
+ Operators.VectorMatchCardinality.OneToOne,
+ new[] {"one", "two"}.ToImmutableArray(),
+ false,
+ ImmutableArray.Empty,
+ false,
+ new TextSpan(input))
+ );
+ }
[Test]
- public void VectorMatching_On() => Parse(Parser.VectorMatching, "on ()")
- .Should().BeEquivalentTo(new VectorMatching(
- Operators.VectorMatchCardinality.OneToOne,
- ImmutableArray.Empty,
- true,
- ImmutableArray.Empty,
- false)
- );
-
+ public void VectorMatching_On()
+ {
+ const string input = "on ()";
+
+ Parse(Parser.VectorMatching, input)
+ .Should().BeEquivalentTo(new VectorMatching(
+ Operators.VectorMatchCardinality.OneToOne,
+ ImmutableArray.Empty,
+ true,
+ ImmutableArray.Empty,
+ false,
+ new TextSpan(input))
+ );
+ }
+
[Test]
- public void VectorMatching_Bool_On() => Parse(Parser.VectorMatching, "bool on ()")
- .Should().BeEquivalentTo(new VectorMatching(
- Operators.VectorMatchCardinality.OneToOne,
- ImmutableArray.Empty,
- true,
- ImmutableArray.Empty,
- true)
- );
-
+ public void VectorMatching_Bool_On()
+ {
+ const string input = "bool on ()";
+ Parse(Parser.VectorMatching, input)
+ .Should().BeEquivalentTo(new VectorMatching(
+ Operators.VectorMatchCardinality.OneToOne,
+ ImmutableArray.Empty,
+ true,
+ ImmutableArray.Empty,
+ true,
+ new TextSpan(input)
+ )
+ );
+ }
+
[Test]
- public void VectorMatching_GroupLeft() => Parse(Parser.VectorMatching, "on () group_left ()")
- .Should().BeEquivalentTo(new VectorMatching(
- Operators.VectorMatchCardinality.ManyToOne,
- ImmutableArray.Empty,
- true,
- ImmutableArray.Empty,
- false)
- );
-
+ public void VectorMatching_GroupLeft()
+ {
+ const string input = "on () group_left ()";
+ Parse(Parser.VectorMatching, input)
+ .Should().BeEquivalentTo(new VectorMatching(
+ Operators.VectorMatchCardinality.ManyToOne,
+ ImmutableArray.Empty,
+ true,
+ ImmutableArray.Empty,
+ false,
+ new TextSpan(input)
+ )
+ );
+ }
+
[Test]
- public void VectorMatching_GroupLeftEmpty() => Parse(Parser.VectorMatching, "on () group_left")
- .Should().BeEquivalentTo(new VectorMatching(
- Operators.VectorMatchCardinality.ManyToOne,
- ImmutableArray.Empty,
- true,
- ImmutableArray.Empty,
- false)
- );
-
+ public void VectorMatching_GroupLeftEmpty()
+ {
+ const string input = "on () group_left";
+
+ Parse(Parser.VectorMatching, input)
+ .Should().BeEquivalentTo(new VectorMatching(
+ Operators.VectorMatchCardinality.ManyToOne,
+ ImmutableArray.Empty,
+ true,
+ ImmutableArray.Empty,
+ false,
+ new TextSpan(input))
+ );
+ }
+
[Test]
- public void VectorMatching_GroupRight() => Parse(Parser.VectorMatching, "on () group_right (one, two)")
- .Should().BeEquivalentTo(new VectorMatching(
- Operators.VectorMatchCardinality.OneToMany,
- ImmutableArray.Empty,
- true,
- new []{ "one","two"}.ToImmutableArray(),
- false)
- );
+ public void VectorMatching_GroupRight()
+ {
+ const string input = "on () group_right (one, two)";
+ Parse(Parser.VectorMatching, input)
+ .Should().BeEquivalentTo(
+ new VectorMatching(
+ Operators.VectorMatchCardinality.OneToMany,
+ ImmutableArray.Empty,
+ true,
+ new []{ "one","two"}.ToImmutableArray(),
+ false,
+ new TextSpan(input)
+ )
+ );
+ }
[Test]
[TestCase("group_left ()")]
@@ -529,7 +727,9 @@ public void VectorMatching_GroupInvalid(string input)
[TestCase("1 unless 1", Operators.Binary.Unless)]
public void BinaryExpr_Operators(string input, Operators.Binary expected)
{
- Parse(Parser.BinaryExpr, input).As().Operator.Should().Be(expected);
+ var parsed = Parse(Parser.BinaryExpr, input).As();
+ parsed.Operator.Should().Be(expected);
+ parsed.Span.Should().Be(new TextSpan(input));
}
[Test]
@@ -544,18 +744,56 @@ public void BinaryExpr_SimpleVector()
}.ToImmutableArray())),
Operators.Binary.Add,
new VectorMatching()
- ));
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
+ );
}
[Test]
- public void BinaryExpr_Repetitive()
+ [TestCase("1 + 2 + 3")]
+ [TestCase("1 / 2 * 3")]
+ [TestCase("1 + 2 - 3")]
+ [TestCase("1 < 2 > 3")]
+ public void BinaryExpr_PrecedenceEqual(string input)
+ {
+ var result = Parse(Parser.BinaryExpr, input) as BinaryExpr;
+ result.RightHandSide.Should().Be(new NumberLiteral(3.0, new TextSpan(input, new Position(8, 0, 0), 1)));
+ var binExpr = result.LeftHandSide.Should().BeOfType();
+
+ binExpr.Which.LeftHandSide.Should().Be(new NumberLiteral(1.0, new TextSpan(input, new Position(0, 0, 0), 1)));
+ binExpr.Which.RightHandSide.Should().Be(new NumberLiteral(2.0, new TextSpan(input, new Position(4, 0, 0), 1)));
+ }
+
+ [Test]
+ [TestCase("1 * 2 + 3")]
+ [TestCase("1 - 2 > 3")]
+ [TestCase("1 < 2 and 3")]
+ [TestCase("1 and 2 or 3")]
+ public void BinaryExpr_PrecedenceHigher(string input)
{
- var result = Parse(Parser.BinaryExpr, "1 + 2 + 3") as BinaryExpr;
- result.LeftHandSide.Should().Be(new NumberLiteral(1.0));
+ var result = Parse(Parser.BinaryExpr, input);
+
+ var binExpr = result.LeftHandSide.Should().BeOfType();
+ binExpr.Which.LeftHandSide.Should().BeOfType().Which.Value.Should().Be(1);
+ binExpr.Which.RightHandSide.Should().BeOfType().Which.Value.Should().Be(2);
+
+ result.RightHandSide.Should().BeOfType().Which.Value.Should().Be(3);
+ }
+
+ [Test]
+ [TestCase("1 - 2 / 3")]
+ [TestCase("1 <= 2 + 3")]
+ [TestCase("1 unless 2 >= 3")]
+ [TestCase("1 or 2 unless 3")]
+ public void BinaryExpr_PrecedenceLower(string input)
+ {
+ var result = Parse(Parser.BinaryExpr, input);
+ result.LeftHandSide.Should().BeOfType().Which.Value.Should().Be(1);
var binExpr = result.RightHandSide.Should().BeOfType();
- binExpr.Which.LeftHandSide.Should().Be(new NumberLiteral(2.0));
- binExpr.Which.RightHandSide.Should().Be(new NumberLiteral(3.0));
+ binExpr.Which.LeftHandSide.Should().BeOfType().Which.Value.Should().Be(2);
+ binExpr.Which.RightHandSide.Should().BeOfType().Which.Value.Should().Be(3);
}
[Test]
@@ -566,78 +804,137 @@ public void BinaryExpr_Nested()
.Expr.Should().BeOfType().Which
.LeftHandSide.Should().BeOfType().Which
.Expr.Should().BeOfType().Which
- .LeftHandSide.Should().Be(new NumberLiteral(1.0));
-
- Parse(Parser.Expr, "(100 * (1) / 128)");
+ .LeftHandSide.Should().BeOfType().Subject.Value.Should().Be(1);
}
[Test]
public void BinaryExpr_VectorMatching()
{
- Parse(Parser.BinaryExpr, "1 + bool 2")
+ Parse(Parser.BinaryExpr, "1 > bool 2")
.Should().BeEquivalentTo(new BinaryExpr(
- new NumberLiteral(1.0),
- new NumberLiteral(2),
- Operators.Binary.Add,
- new VectorMatching(true)
- ));
+ new NumberLiteral(1.0),
+ new NumberLiteral(2),
+ Operators.Binary.Gtr,
+ new VectorMatching(true)
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
+ );
+ }
+
+ [Test]
+ [TestCase("1 + bool 2")]
+ [TestCase("1 - bool 2")]
+ [TestCase("1 / bool 2")]
+ [TestCase("1 * bool 2")]
+ [TestCase("1 or bool 2")]
+ [TestCase("1 unless bool 2")]
+ [TestCase("1 and bool 2")]
+ public void BinaryExpr_BoolNonComparison(string query)
+ {
+ Assert.Throws(() => Parse(Parser.BinaryExpr, "1 + bool 2"))
+ .Message.Should().Contain("bool modifier can only be used on comparison operators");
}
[Test]
[TestCase("avg (blah)", "avg")]
- [TestCase("bottomk (blah)", "bottomk")]
+ [TestCase("bottomk (2, blah)", "bottomk")]
[TestCase("count (blah)", "count")]
- [TestCase("count_values (blah)", "count_values")]
+ [TestCase("count_values (blah, values)", "count_values")]
[TestCase("group (blah)", "group")]
[TestCase("max (blah)", "max")]
[TestCase("min (blah)", "min")]
- [TestCase("quantile (blah)", "quantile")]
+ [TestCase("quantile (0.5, blah)", "quantile")]
[TestCase("stddev (blah)", "stddev")]
[TestCase("stdvar (blah)", "stdvar")]
[TestCase("sum (blah)", "sum")]
- [TestCase("topk (blah)", "topk")]
+ [TestCase("topk (1, blah)", "topk")]
public void AggregateExpr_Operator(string input, string expected) => Parse(Parser.AggregateExpr, input)
- .OperatorName.Should().Be(expected);
+ .Operator.Name.Should().Be(expected);
[Test]
- public void AggregateExpr_NoMod() => Parse(Parser.AggregateExpr, "sum (blah)").Should().BeEquivalentTo(
- new AggregateExpr(
- "sum",
- new VectorSelector(new MetricIdentifier("blah")),
- null,
- ImmutableArray.Empty,
- false
- ));
-
+ public void AggregateExpr_NoMod()
+ {
+ const string input = "sum (blah)";
+
+ var result = Parse(Parser.AggregateExpr, input);
+ result.Should().BeEquivalentTo(
+ new AggregateExpr(
+ Operators.Aggregates["sum"],
+ new VectorSelector(new MetricIdentifier("blah")),
+ null,
+ ImmutableArray.Empty,
+ false
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
+ );
+
+ result.Span.Should().Be(new TextSpan(input));
+ }
+
[Test]
- public void AggregateExpr_LeadingModBy() => Parse(Parser.AggregateExpr, "sum by (one, two) (blah)").Should().BeEquivalentTo(
- new AggregateExpr(
- "sum",
- new VectorSelector(new MetricIdentifier("blah")),
- null,
- new string[] {"one", "two"}.ToImmutableArray(),
- false
- ));
-
+ public void AggregateExpr_LeadingModBy()
+ {
+ const string input = "sum by (one, two) (blah)";
+ var result = Parse(Parser.AggregateExpr, input);
+
+ result.Should().BeEquivalentTo(
+ new AggregateExpr(
+ Operators.Aggregates["sum"],
+ new VectorSelector(new MetricIdentifier("blah")),
+ null,
+ new string[] {"one", "two"}.ToImmutableArray(),
+ false
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
+ );
+ result.Span.Should().Be(new TextSpan(input));
+ }
+
[Test]
- public void AggregateExpr_TrailingModWithout() => Parse(Parser.AggregateExpr, "sum (blah) without (one)").Should().BeEquivalentTo(
- new AggregateExpr(
- "sum",
- new VectorSelector(new MetricIdentifier("blah")),
- null,
- new string[] {"one" }.ToImmutableArray(),
- true
- ));
-
+ public void AggregateExpr_TrailingModWithout()
+ {
+ const string input = "sum (blah) without (one)";
+
+ var result = Parse(Parser.AggregateExpr, input);
+ result.Should().BeEquivalentTo(
+ new AggregateExpr(
+ Operators.Aggregates["sum"],
+ new VectorSelector(new MetricIdentifier("blah")),
+ null,
+ new string[] {"one"}.ToImmutableArray(),
+ true
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
+ );
+ result.Span.Should().Be(new TextSpan(input));
+ }
+
[Test]
public void AggregateExpr_TwoArgs() => Parse(Parser.AggregateExpr, "quantile (0.5, blah)").Should().BeEquivalentTo(
new AggregateExpr(
- "quantile",
+ Operators.Aggregates["quantile"],
new VectorSelector(new MetricIdentifier("blah")),
new NumberLiteral(0.5),
ImmutableArray.Empty,
false
- ));
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
+ );
+
+ [Test]
+ public void AggregateExpr_OneArgs_InvalidFunc() =>
+ Assert.Throws(() => Parse(Parser.AggregateExpr, "quantile (blah)"))
+ .Message.Should().Contain("wrong number of arguments for aggregate expression provided, expected 2, got 1");
+
+ [Test]
+ public void AggregateExpr_TwoArgs_InvalidFunc() =>
+ Assert.Throws(() => Parse(Parser.AggregateExpr, "sum (0.5, blah)"))
+ .Message.Should().Contain("wrong number of arguments for aggregate expression provided, expected 1, got 2");
[Test]
public void AggregateExpr_LabelNameAggOp()
@@ -646,38 +943,58 @@ public void AggregateExpr_LabelNameAggOp()
}
[Test]
- public void Subquery_WithStep() => Parse(Parser.Expr, "blah[1h:1m]").Should().BeEquivalentTo(
- new SubqueryExpr(
- new VectorSelector(new MetricIdentifier("blah")),
- new Duration(TimeSpan.FromHours(1)),
- new Duration(TimeSpan.FromMinutes(1))
- ));
-
- [Test]
- public void Subquery_WithoutStep() => Parse(Parser.Expr, "blah[1d:]").Should().BeEquivalentTo(
- new SubqueryExpr(
- new VectorSelector(new MetricIdentifier("blah")),
- new Duration(TimeSpan.FromDays(1)),
- null
- ));
-
- [Test]
- public void Subquery_Expressions()
- {
- Parse(Parser.Expr, "vector(1) [1h:]").Should().BeOfType().Which
+ public void Subquery_WithStep()
+ {
+ const string input = "blah[1h:1m]";
+ var result = Parse(Parser.Expr, input);
+
+ result.Should().BeEquivalentTo(
+ new SubqueryExpr(
+ new VectorSelector(new MetricIdentifier("blah")),
+ new Duration(TimeSpan.FromHours(1)),
+ new Duration(TimeSpan.FromMinutes(1))
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
+ );
+ result.Span.Should().Be(new TextSpan(input));
+ }
+
+ [Test]
+ public void Subquery_WithoutStep()
+ {
+ const string input = "blah[1d:]";
+ var result = Parse(Parser.Expr, input);
+
+ result.Should().BeEquivalentTo(
+ new SubqueryExpr(
+ new VectorSelector(new MetricIdentifier("blah")),
+ new Duration(TimeSpan.FromDays(1)),
+ null
+ ),
+ // Don't assert over parsed Span positions, will be tedious to specify all positions
+ cfg => cfg.Excluding(x => x.Name == "Span")
+ );
+ result.Span.Should().Be(new TextSpan(input));
+ }
+
+ [Test]
+ public void Subquery_Expressions()
+ {
+ Parse(Parser.Expr, "vector(1) [1h:]").Should().BeOfType().Which
.Expr.Should().BeOfType();
-
- Parse(Parser.Expr, "1 + 1 [1h:]").Should().BeOfType().Which
+
+ Parse(Parser.Expr, "1 + 1 [1h:]").Should().BeOfType().Which
.RightHandSide.Should().BeOfType();
-
- Parse(Parser.Expr, "(1 + 1) [1h:]").Should().BeOfType().Which
+
+ Parse(Parser.Expr, "(1 + 1) [1h:]").Should().BeOfType().Which
.Expr.Should().BeOfType();
-
- Parse(Parser.Expr, "blah{} [1h:]").Should().BeOfType().Which
- .Expr.Should().BeOfType();
- }
- public static T Parse(TokenListParser parser, string input)
+ Parse(Parser.Expr, "blah{} [1h:]").Should().BeOfType().Which
+ .Expr.Should().BeOfType();
+ }
+
+ public static T Parse(TokenListParser parser, string input)
{
var tokens = new Tokenizer().Tokenize(input);
try
diff --git a/tests/PromQL.Parser.Tests/PrinterTests.cs b/tests/PromQL.Parser.Tests/PrinterTests.cs
index f3015c1..8d8927e 100644
--- a/tests/PromQL.Parser.Tests/PrinterTests.cs
+++ b/tests/PromQL.Parser.Tests/PrinterTests.cs
@@ -54,7 +54,7 @@ public void NumberLiteral_Inf_ToPromQl()
[Test]
public void Aggregate_ToPromQl()
{
- _printer.ToPromQl(new AggregateExpr("sum",
+ _printer.ToPromQl(new AggregateExpr(Operators.Aggregates["sum"],
new VectorSelector(new MetricIdentifier("test_expr")),
null,
ImmutableArray.Empty,
@@ -65,7 +65,7 @@ public void Aggregate_ToPromQl()
[Test]
public void Aggregate_WithLabels_ToPromQl()
{
- _printer.ToPromQl(new AggregateExpr("sum",
+ _printer.ToPromQl(new AggregateExpr(Operators.Aggregates["sum"],
new VectorSelector(new MetricIdentifier("test_expr")),
null,
new [] { "one" }.ToImmutableArray(),
@@ -76,7 +76,7 @@ public void Aggregate_WithLabels_ToPromQl()
[Test]
public void Aggregate_WithParam_ToPromQl()
{
- _printer.ToPromQl(new AggregateExpr("quantile",
+ _printer.ToPromQl(new AggregateExpr(Operators.Aggregates["quantile"],
new VectorSelector(new MetricIdentifier("test_expr")),
new NumberLiteral(1),
new[] {"label1", "label2"}.ToImmutableArray(),
@@ -181,13 +181,13 @@ public void Complex_ToPromQl() =>
),
new UnaryExpr(
Operators.Unary.Sub,
- new FunctionCall("sum", new Expr[]
+ new FunctionCall(Functions.Map["vector"], new Expr[]
{
new OffsetExpr(new VectorSelector(new MetricIdentifier("this_is_a_metric")), new Duration(TimeSpan.FromMinutes(5)))
}.ToImmutableArray())
),
Operators.Binary.Add,
null
- )).Should().Be("(another_metric{one='test', two!='test2'}[1h][1d:5m]) + -sum(this_is_a_metric offset 5m)");
+ )).Should().Be("(another_metric{one='test', two!='test2'}[1h][1d:5m]) + -vector(this_is_a_metric offset 5m)");
}
}
\ No newline at end of file
diff --git a/tests/PromQL.Parser.Tests/TypeCheckerTests.cs b/tests/PromQL.Parser.Tests/TypeCheckerTests.cs
new file mode 100644
index 0000000..f8aa0ad
--- /dev/null
+++ b/tests/PromQL.Parser.Tests/TypeCheckerTests.cs
@@ -0,0 +1,192 @@
+using System;
+using FluentAssertions;
+using NUnit.Framework;
+
+namespace PromQL.Parser.Tests
+{
+ [TestFixture]
+ public class TypeCheckerTests
+ {
+ [Test]
+ public void Single_Expected_Type_Not_Found()
+ {
+ Assert.Throws(() => Parser.ParseExpression("sum_over_time(instant_vector)").CheckType())
+ .Message.Should().Be("Unexpected type 'instant vector' was provided, expected range vector: 14 (line 1, column 15)");
+ }
+
+ [Test]
+ public void Multiple_Expected_Types_Not_Found()
+ {
+ Assert.Throws(() => Parser.ParseExpression("+'hello'").CheckType())
+ .Message.Should().Be("Unexpected type 'string' was provided, expected one of 'scalar', 'instant vector': 1 (line 1, column 2)");
+ }
+
+ [Test]
+ [TestCase("sum_over_time(instant_vector)")]
+ [TestCase("hour(range_vector[1h])")]
+ public void FunctionCall_InvalidArgumentTypes(string expr)
+ {
+ Assert.Throws(() => Parser.ParseExpression(expr).CheckType());
+ }
+
+ [Test]
+ [TestCase("sum_over_time(range_vector[1d])", ValueType.Vector)]
+ [TestCase("hour()", ValueType.Vector)]
+ [TestCase("hour(instant_vector)", ValueType.Vector)]
+ [TestCase("hour(instant_vector, instant_vector2, instant_vector3)", ValueType.Vector)]
+ public void FunctionCall_ValidArgumentTypes(string expr, ValueType expected)
+ {
+ Parser.ParseExpression(expr).CheckType()
+ .Should().Be(expected);
+ }
+
+ [Test]
+ [TestCase("-'a'")]
+ [TestCase("-some_matrix[1d]")]
+ public void UnaryExpr_InvalidArgumentTypes(string expr)
+ {
+ Assert.Throws(() => Parser.ParseExpression(expr).CheckType());
+ }
+
+ [Test]
+ [TestCase("-1", ValueType.Scalar)]
+ [TestCase("-some_vector", ValueType.Vector)]
+ public void UnaryExpr_ValidArgumentTypes(string expr, ValueType expected)
+ {
+ Parser.ParseExpression(expr).CheckType()
+ .Should().Be(expected);
+ }
+
+ [Test]
+ [TestCase("some_matrix[1d][1d:1h]")]
+ [TestCase("'string'[1d:1h]")]
+ [TestCase("1[1d:1h]")]
+ [TestCase("time()[1d:1h]")]
+ public void SubqueryExpr_InvalidArgumentTypes(string expr)
+ {
+ Assert.Throws(() => Parser.ParseExpression(expr).CheckType());
+ }
+
+ [Test]
+ [TestCase("some_vector[1d:1h]", ValueType.Matrix)]
+ [TestCase("hour()[1d:1h]", ValueType.Matrix)]
+ public void SubqueryExpr_ValidArgumentTypes(string expr, ValueType expected)
+ {
+ Parser.ParseExpression(expr).CheckType()
+ .Should().Be(expected);
+ }
+
+ [Test]
+ public void BinaryExpr_InvalidLhsString()
+ {
+ Assert.Throws(
+ () => Parser.ParseExpression("'a' + 1 + 1 + 1").CheckType()
+ )
+ .Message.Should().Contain("Unexpected type 'string' was provided, expected one of 'scalar', 'instant vector'");
+ }
+
+ [Test]
+ public void BinaryExpr_InvalidRhsString()
+ {
+ Assert.Throws(
+ () => Parser.ParseExpression("1 + 1 + 1 + 'a'").CheckType()
+ )
+ .Message.Should().Contain("Unexpected type 'string' was provided, expected one of 'scalar', 'instant vector'");
+ }
+
+ [Test]
+ public void BinaryExpr_InvalidLhsMatrix()
+ {
+ Assert.Throws(
+ () => Parser.ParseExpression("some_matrix[1d] + 1 + 1 + 1").CheckType()
+ )
+ .Message.Should().Contain("Unexpected type 'range vector' was provided, expected one of 'scalar', 'instant vector'");
+ }
+
+ [Test]
+ public void BinaryExpr_InvalidRhsMatrix()
+ {
+ Assert.Throws(
+ () => Parser.ParseExpression("1 + 1 + 1 + some_matrix[1d]").CheckType()
+ )
+ .Message.Should().Contain("Unexpected type 'range vector' was provided, expected one of 'scalar', 'instant vector'");
+ }
+
+ [Test]
+ public void BinaryExpr_Scalar()
+ {
+ Parser.ParseExpression("1 > bool 1").CheckType().Should().Be(ValueType.Scalar);
+ }
+
+ [Test]
+ public void BinaryExpr_Vector()
+ {
+ Parser.ParseExpression("first_vector and second_vector").CheckType().Should().Be(ValueType.Vector);
+ }
+
+ [Test]
+ public void BinaryExpr_VectorScalar()
+ {
+ Parser.ParseExpression("first_vector > 1").CheckType().Should().Be(ValueType.Vector);
+ }
+
+ [Test]
+ public void BinaryExpr_VectorScalarSet()
+ {
+ Assert.Throws(
+ () => Parser.ParseExpression("first_vector and 1").CheckType()
+ )
+ .Message.Should().Contain("set operator And not allowed in binary scalar expression");
+ }
+
+ [Test]
+ public void BinaryExpr_ScalarNoBool()
+ {
+ Assert.Throws(
+ () => Parser.ParseExpression("1 > 1").CheckType()
+ ).Message.Should().Contain("comparisons between scalars must use bool modifier");
+ }
+
+ [Test]
+ public void BinarExpr_Associativity()
+ {
+ Parser.ParseExpression("up == 1 unless on (instance) (test)").CheckType()
+ .Should().Be(ValueType.Vector);
+ }
+
+ [Test]
+ public void BinarExpr_Precedence()
+ {
+ Console.WriteLine(Parser.ParseExpression(@"(metric1) > 80 and 100 - (metric2) < 90"));
+ Parser.ParseExpression(@"(metric1) > 80 and 100 - (metric2) < 90").CheckType()
+ .Should().Be(ValueType.Vector);
+ }
+
+ [Test]
+ public void ParenExpression_Nested()
+ {
+ Assert.Throws(
+ () => Parser.ParseExpression("((((-'a'))))").CheckType()
+ )
+ .Message.Should().Be("Unexpected type 'string' was provided, expected one of 'scalar', 'instant vector': 5 (line 1, column 6)");
+ }
+
+ [Test]
+ [TestCase("sum('one')", "'string' was provided, expected instant vector")]
+ [TestCase("avg(1)", "'scalar' was provided, expected instant vector")]
+ [TestCase("max(scalar(1))", "'scalar' was provided, expected instant vector")]
+ [TestCase("min(blah[2m])", "'range vector' was provided, expected instant vector")]
+ [TestCase("quantile('1', test)", "'string' was provided, expected scalar")]
+ [TestCase("quantile(1, test[1m])", "'range vector' was provided, expected instant vector")]
+ [TestCase("topk(2, test[1m])", "'range vector' was provided, expected instant vector")]
+ [TestCase("bottomk(one, '2')", "'instant vector' was provided, expected scalar")]
+ [TestCase("count_values(one, 'one')", "'instant vector' was provided, expected string")]
+ public void AggregateExpression_InvalidParameterTypes(string input, string expected)
+ {
+ Assert.Throws(
+ () => Parser.ParseExpression(input).CheckType()
+ )
+ .Message.Should().Contain(expected);
+ }
+ }
+}
\ No newline at end of file