// Copyright 2021 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { Parser } from './parser'; import { Diagnostic } from '@codemirror/lint'; import { createEditorState } from '../test/utils-test'; import { syntaxTree } from '@codemirror/language'; import { ValueType } from '../types'; describe('promql operations', () => { const testCases = [ { expr: '1', expectedValueType: ValueType.scalar, expectedDiag: [] as Diagnostic[], }, { expr: '2 * 3', expectedValueType: ValueType.scalar, expectedDiag: [] as Diagnostic[], }, { expr: '1 unless 1', expectedValueType: ValueType.scalar, expectedDiag: [ { from: 0, to: 10, message: 'set operator not allowed in binary scalar expression', severity: 'error', }, ], }, { expr: 'metric_name * "string"', expectedValueType: ValueType.vector, expectedDiag: [ { from: 14, to: 22, message: 'binary expression must contain only scalar and instant vector types', severity: 'error', }, ] as Diagnostic[], }, { expr: 'metric_name_1 > bool metric_name_2', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'metric_name_1 + bool metric_name_2', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 34, message: 'bool modifier can only be used on comparison operators', severity: 'error', }, ] as Diagnostic[], }, { expr: 'metric_name offset 1d', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'metric_name[5m] offset 1d', expectedValueType: ValueType.matrix, expectedDiag: [] as Diagnostic[], }, { expr: 'rate(metric_name[5m])[1h:] offset 1m', expectedValueType: ValueType.matrix, expectedDiag: [] as Diagnostic[], }, { expr: 'sum(metric_name offset 1m)', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'rate(metric_name[5m] offset 1d)', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'mad_over_time(rate(metric_name[5m])[1h:] offset 1m)', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'max_over_time(rate(metric_name[5m])[1h:] offset 1m)', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo * bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo*bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo* bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo *bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo==bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo * sum', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo == 1', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo == bool 1', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: '2.5 / bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo and bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo or bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo unless bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { // Test and/or precedence and reassigning of operands. // Here it will test only the first VectorMatching so (a + b) or (c and d) ==> ManyToMany expr: 'foo + bar or bla and blub', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { // Test and/or/unless precedence. // Here it will test only the first VectorMatching so ((a and b) unless c) or d ==> ManyToMany expr: 'foo and bar unless baz or qux', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo * on(test,blub) bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo*on(test,blub)bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo * on(test,blub) group_left bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo*on(test,blub)group_left()bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo and on(test,blub) bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo and on(test,"blub") bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo and on() bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo and ignoring(test,blub) bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo and ignoring(test,"blub") bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo and ignoring() bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo unless on(bar) baz', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo / on(test,blub) group_left(bar) bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo / on(test,blub) group_left("bar") bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo / ignoring(test,blub) group_left(blub) bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo / ignoring(test,blub) group_left(bar) bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo - on(test,blub) group_right(bar,foo) bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo - ignoring(test,blub) group_right(bar,foo) bar', expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, { expr: 'foo and 1', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 9, message: 'set operator not allowed in binary scalar expression', severity: 'error', }, ], }, { expr: '1 and foo', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 9, message: 'set operator not allowed in binary scalar expression', severity: 'error', }, ], }, { expr: 'foo or 1', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 8, message: 'set operator not allowed in binary scalar expression', severity: 'error', }, ], }, { expr: '1 or foo', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 8, message: 'set operator not allowed in binary scalar expression', severity: 'error', }, ], }, { expr: 'foo unless 1', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 12, message: 'set operator not allowed in binary scalar expression', severity: 'error', }, ], }, { expr: '1 unless foo', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 12, message: 'set operator not allowed in binary scalar expression', severity: 'error', }, ], }, { expr: '1 or on(bar) foo', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 16, message: 'vector matching only allowed between instant vectors', severity: 'error', }, { from: 0, to: 16, message: 'set operator not allowed in binary scalar expression', severity: 'error', }, ], }, { expr: 'foo == on(bar) 10', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 17, message: 'vector matching only allowed between instant vectors', severity: 'error', }, ], }, { expr: 'foo and on(bar) group_left(baz) bar', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 35, message: 'no grouping allowed for set operations', severity: 'error', }, { from: 0, to: 35, message: 'set operations must always be many-to-many', severity: 'error', }, ], }, { expr: 'foo and on(bar) group_right(baz) bar', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 36, message: 'no grouping allowed for set operations', severity: 'error', }, { from: 0, to: 36, message: 'set operations must always be many-to-many', severity: 'error', }, ], }, { expr: 'foo or on(bar) group_left(baz) bar', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 34, message: 'no grouping allowed for set operations', severity: 'error', }, { from: 0, to: 34, message: 'set operations must always be many-to-many', severity: 'error', }, ], }, { expr: 'foo or on(bar) group_right(baz) bar', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 35, message: 'no grouping allowed for set operations', severity: 'error', }, { from: 0, to: 35, message: 'set operations must always be many-to-many', severity: 'error', }, ], }, { expr: 'foo unless on(bar) group_left(baz) bar', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 38, message: 'no grouping allowed for set operations', severity: 'error', }, { from: 0, to: 38, message: 'set operations must always be many-to-many', severity: 'error', }, ], }, { expr: 'foo unless on(bar) group_right(baz) bar', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 39, message: 'no grouping allowed for set operations', severity: 'error', }, { from: 0, to: 39, message: 'set operations must always be many-to-many', severity: 'error', }, ], }, { expr: 'http_requests{group="production"} + on(instance) group_left(job,instance) cpu_count{type="smp"}', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 95, message: 'label "instance" must not occur in ON and GROUP clause at once', severity: 'error', }, ], }, { expr: 'foo + bool bar', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 14, message: 'bool modifier can only be used on comparison operators', severity: 'error', }, ], }, { expr: 'foo + bool 10', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 13, message: 'bool modifier can only be used on comparison operators', severity: 'error', }, ], }, { expr: 'foo and bool 10', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 15, message: 'bool modifier can only be used on comparison operators', severity: 'error', }, { from: 0, to: 15, message: 'set operator not allowed in binary scalar expression', severity: 'error', }, ], }, // test aggregration { expr: 'sum by (foo)(some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'avg by (foo)(some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'max by (foo)(some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'sum without (foo) (some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'sum (some_metric) without (foo)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'stddev(some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'stdvar by (foo)(some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'sum by ()(some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'sum by (foo,bar,)(some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'sum by (foo,)(some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'topk(5, some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'topk( # my awesome comment\n' + '5, some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'count_values("value", some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'sum without(and, by, avg, count, alert, annotations)(some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'sum some_metric by (test)', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 25, message: 'unable to find the parameter for the expression', severity: 'error', }, ], }, // Test function calls. { expr: 'time()', expectedValueType: ValueType.scalar, expectedDiag: [], }, { expr: 'floor(some_metric{foo!="bar"})', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'rate(some_metric[5m])', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'round(some_metric)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'round(some_metric, 5)', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'floor()', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 7, message: 'expected 1 argument(s) in call to "floor", got 0', severity: 'error', }, ], }, { expr: 'floor(some_metric, other_metric)', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 32, message: 'expected 1 argument(s) in call to "floor", got 2', severity: 'error', }, ], }, { expr: 'floor(some_metric, 1)', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 21, message: 'expected 1 argument(s) in call to "floor", got 2', severity: 'error', }, ], }, { expr: 'floor(1)', expectedValueType: ValueType.vector, expectedDiag: [ { from: 6, to: 7, message: 'expected type vector in call to function "floor", got scalar', severity: 'error', }, ], }, { expr: 'hour(some_metric, some_metric, some_metric)', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 43, message: 'expected at most 1 argument(s) in call to "hour", got 3', severity: 'error', }, ], }, { expr: 'time(some_metric)', expectedValueType: ValueType.scalar, expectedDiag: [ { from: 0, to: 17, message: 'expected 0 argument(s) in call to "time", got 1', severity: 'error', }, ], }, { expr: 'rate(some_metric)', expectedValueType: ValueType.vector, expectedDiag: [ { from: 5, to: 16, message: 'expected type matrix in call to function "rate", got vector', severity: 'error', }, ], }, { expr: 'histogram_quantile( # Root of the query, final result, approximates a quantile.\n' + ' 0.9, # 1st argument to histogram_quantile(), the target quantile.\n' + ' sum by(le, method, path) ( # 2nd argument to histogram_quantile(), an aggregated histogram.\n' + ' rate( # Argument to sum(), the per-second increase of a histogram over 5m.\n' + ' demo_api_request_duration_seconds_bucket{job="demo"}[5m] # Argument to rate(), the raw histogram series over the last 5m.\n' + ' )\n' + ' )\n' + ')', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'histogram_fraction( # Root of the query, final result, approximates a fraction of observations within an interval.\n' + ' -Inf, # 1st argument to histogram_fraction(), start of the interval.\n' + ' 123.4, # 2nd argument to histogram_fraction(), end of the interval.\n' + ' sum by(method, path) ( # 3rd argument to histogram_fraction(), an aggregated histogram.\n' + ' rate( # Argument to sum(), the per-second increase of a histogram over 5m.\n' + ' demo_api_request_duration_seconds{job="demo"}[5m] # Argument to rate(), a vector of sparse histogram series over the last 5m.\n' + ' )\n' + ' )\n' + ')', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'histogram_count( # Root of the query, final result, returns the count of observations.\n' + ' sum by(method, path) ( # Argument to histogram_count(), an aggregated histogram.\n' + ' rate( # Argument to sum(), the per-second increase of a histogram over 5m.\n' + ' demo_api_request_duration_seconds{job="demo"}[5m] # Argument to rate(), a vector of sparse histogram series over the last 5m.\n' + ' )\n' + ' )\n' + ')', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'histogram_sum( # Root of the query, final result, returns the sum of observations.\n' + ' sum by(method, path) ( # Argument to histogram_sum(), an aggregated histogram.\n' + ' rate( # Argument to sum(), the per-second increase of a histogram over 5m.\n' + ' demo_api_request_duration_seconds{job="demo"}[5m] # Argument to rate(), a vector of sparse histogram series over the last 5m.\n' + ' )\n' + ' )\n' + ')', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'histogram_avg( # Root of the query, final result, returns the average of observations.\n' + ' sum by(method, path) ( # Argument to histogram_avg(), an aggregated histogram.\n' + ' rate( # Argument to sum(), the per-second increase of a histogram over 5m.\n' + ' demo_api_request_duration_seconds{job="demo"}[5m] # Argument to rate(), a vector of sparse histogram series over the last 5m.\n' + ' )\n' + ' )\n' + ')', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'histogram_stddev( # Root of the query, final result, returns the standard deviation of observations.\n' + ' sum by(method, path) ( # Argument to histogram_stddev(), an aggregated histogram.\n' + ' rate( # Argument to sum(), the per-second increase of a histogram over 5m.\n' + ' demo_api_request_duration_seconds{job="demo"}[5m] # Argument to rate(), a vector of sparse histogram series over the last 5m.\n' + ' )\n' + ' )\n' + ')', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'histogram_stdvar( # Root of the query, final result, returns the standard variance of observations.\n' + ' sum by(method, path) ( # Argument to histogram_stdvar(), an aggregated histogram.\n' + ' rate( # Argument to sum(), the per-second increase of a histogram over 5m.\n' + ' demo_api_request_duration_seconds{job="demo"}[5m] # Argument to rate(), a vector of sparse histogram series over the last 5m.\n' + ' )\n' + ' )\n' + ')', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: '1 @ start()', expectedValueType: ValueType.scalar, expectedDiag: [ { from: 0, to: 11, message: '@ modifier must be preceded by an instant selector vector or range vector selector or a subquery', severity: 'error', }, ], }, { expr: 'foo @ 879', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'food @ start()', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'food @ end()', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'sum (rate(foo[5m])) @ 456', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: '{"foo"}', expectedValueType: ValueType.vector, expectedDiag: [], }, { // with metric name in the middle expr: '{a="b","foo",c~="d"}', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: '{"foo", a="bc"}', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: '{"colon:in:the:middle"}', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: '{"dot.in.the.middle"}', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: '{"😀 in metric name"}', expectedValueType: ValueType.vector, expectedDiag: [], }, { // quotes with escape expr: '{"this is \"foo\" metric"}', // eslint-disable-line expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: '{"foo","colon:in:the:middle"="val"}', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: '{"foo","dot.in.the.middle"="val"}', expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: '{"foo","😀 in label name"="val"}', expectedValueType: ValueType.vector, expectedDiag: [], }, { // quotes with escape expr: '{"foo","this is \"bar\" label"="val"}', // eslint-disable-line expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: 'foo{"bar"}', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, message: 'metric name must not be set twice: foo or bar', severity: 'error', to: 10, }, ], }, { expr: '{"foo", __name__="bar"}', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, message: 'metric name must not be set twice: foo or bar', severity: 'error', to: 23, }, ], }, { expr: '{"foo", "__name__"="bar"}', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, message: 'metric name must not be set twice: foo or bar', severity: 'error', to: 25, }, ], }, { expr: '{"__name__"="foo", __name__="bar"}', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, message: 'metric name must not be set twice: foo or bar', severity: 'error', to: 34, }, ], }, { expr: '{"foo", "bar"}', expectedValueType: ValueType.vector, expectedDiag: [ { from: 0, to: 14, message: 'metric name must not be set twice: foo or bar', severity: 'error', }, ], }, { expr: `{'foo\`metric':'bar'}`, // eslint-disable-line expectedValueType: ValueType.vector, expectedDiag: [], }, { expr: '{`foo\"metric`=`bar`}', // eslint-disable-line expectedValueType: ValueType.vector, expectedDiag: [], }, ]; testCases.forEach((value) => { const state = createEditorState(value.expr); const parser = new Parser(state); it(value.expr, () => { expect(parser.checkAST(syntaxTree(state).topNode.firstChild)).toEqual(value.expectedValueType); expect(parser.getDiagnostics()).toEqual(value.expectedDiag); }); }); });