mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-19 23:42:29 +00:00
Compare commits
5 Commits
fix/openap
...
tvats-add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9102e4ccc6 | ||
|
|
74b1df2941 | ||
|
|
a8348b6395 | ||
|
|
2aedf5f7e6 | ||
|
|
a77a4d4daa |
188
grammar/HavingExpression.g4
Normal file
188
grammar/HavingExpression.g4
Normal file
@@ -0,0 +1,188 @@
|
||||
grammar HavingExpression;
|
||||
|
||||
/*
|
||||
* Parser Rules
|
||||
*/
|
||||
|
||||
query
|
||||
: expression EOF
|
||||
;
|
||||
|
||||
// Expression with standard boolean precedence:
|
||||
// - parentheses > NOT > AND > OR
|
||||
expression
|
||||
: orExpression
|
||||
;
|
||||
|
||||
// OR expressions
|
||||
orExpression
|
||||
: andExpression ( OR andExpression )*
|
||||
;
|
||||
|
||||
// AND expressions
|
||||
andExpression
|
||||
: primary ( AND primary )*
|
||||
;
|
||||
|
||||
// Primary: an optionally negated parenthesized expression, or a comparison.
|
||||
// NOT is only allowed on grouped expressions, not on bare comparisons.
|
||||
// E.g.: NOT (count() > 100 AND sum(bytes) < 500)
|
||||
primary
|
||||
: NOT? LPAREN orExpression RPAREN
|
||||
| comparison
|
||||
;
|
||||
|
||||
/*
|
||||
* Comparison between two arithmetic operands.
|
||||
* E.g.: count() > 100, total_duration >= 500, __result_0 != 0
|
||||
*/
|
||||
comparison
|
||||
: operand compOp operand
|
||||
;
|
||||
|
||||
compOp
|
||||
: EQUALS
|
||||
| NOT_EQUALS
|
||||
| NEQ
|
||||
| LT
|
||||
| LE
|
||||
| GT
|
||||
| GE
|
||||
;
|
||||
|
||||
/*
|
||||
* Operands support additive arithmetic (+/-).
|
||||
* E.g.: sum(a) + sum(b) > 1000, count() - 10 > 0
|
||||
*/
|
||||
operand
|
||||
: operand (PLUS | MINUS) term
|
||||
| term
|
||||
;
|
||||
|
||||
/*
|
||||
* Terms support multiplicative arithmetic (*, /, %)
|
||||
* E.g.: count() * 2 > 100, sum(bytes) / 1024 > 10
|
||||
*/
|
||||
term
|
||||
: term (STAR | SLASH | PERCENT) factor
|
||||
| factor
|
||||
;
|
||||
|
||||
/*
|
||||
* Factors: atoms or parenthesized operands for arithmetic grouping.
|
||||
* E.g.: (sum(a) + sum(b)) * 2 > 100
|
||||
*/
|
||||
factor
|
||||
: LPAREN operand RPAREN
|
||||
| atom
|
||||
;
|
||||
|
||||
/*
|
||||
* Atoms are the basic building blocks of arithmetic operands:
|
||||
* - aggregate function calls: count(), sum(bytes), avg(duration)
|
||||
* - identifier references: aliases, result refs (__result, __result_0, __result0)
|
||||
* - numeric literals: 100, 0.5, 1e6
|
||||
*
|
||||
* Quoted string literals are intentionally excluded — HAVING expressions
|
||||
* compare aggregate results which are always numeric.
|
||||
*/
|
||||
atom
|
||||
: functionCall
|
||||
| identifier
|
||||
| NUMBER
|
||||
;
|
||||
|
||||
/*
|
||||
* Aggregate function calls:
|
||||
* - count()
|
||||
* - sum(bytes)
|
||||
* - avg(duration_nano)
|
||||
*
|
||||
* Arguments are restricted to a single column identifier.
|
||||
* Nested function calls are not valid in HAVING expressions —
|
||||
* reference nested aggregations by alias or expression string instead.
|
||||
*/
|
||||
functionCall
|
||||
: IDENTIFIER LPAREN functionArgs? RPAREN
|
||||
;
|
||||
|
||||
functionArgs
|
||||
: functionArg ( COMMA functionArg )*
|
||||
;
|
||||
|
||||
// Function argument: a column being aggregated (e.g. bytes, duration_nano)
|
||||
functionArg
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
// Identifier references: aliases, field names, result references
|
||||
// Examples: total_logs, error_count, __result, __result_0, __result0, p99
|
||||
identifier
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
/*
|
||||
* Lexer Rules
|
||||
*/
|
||||
|
||||
// Punctuation
|
||||
LPAREN : '(' ;
|
||||
RPAREN : ')' ;
|
||||
COMMA : ',' ;
|
||||
|
||||
// Comparison operators
|
||||
EQUALS : '=' | '==' ;
|
||||
NOT_EQUALS : '!=' ;
|
||||
NEQ : '<>' ; // alternate not-equals operator
|
||||
LT : '<' ;
|
||||
LE : '<=' ;
|
||||
GT : '>' ;
|
||||
GE : '>=' ;
|
||||
|
||||
// Arithmetic operators
|
||||
PLUS : '+' ;
|
||||
MINUS : '-' ;
|
||||
STAR : '*' ;
|
||||
SLASH : '/' ;
|
||||
PERCENT : '%' ;
|
||||
|
||||
// Boolean logic (case-insensitive)
|
||||
NOT : [Nn][Oo][Tt] ;
|
||||
AND : [Aa][Nn][Dd] ;
|
||||
OR : [Oo][Rr] ;
|
||||
|
||||
// Boolean constants (case-insensitive)
|
||||
BOOL
|
||||
: [Tt][Rr][Uu][Ee]
|
||||
| [Ff][Aa][Ll][Ss][Ee]
|
||||
;
|
||||
|
||||
fragment SIGN : [+-] ;
|
||||
|
||||
// Numbers: optional sign, digits, optional decimal, optional scientific notation
|
||||
// E.g.: 100, -10, 0.5, 1.5e3, .75, -3.14
|
||||
NUMBER
|
||||
: SIGN? DIGIT+ ('.' DIGIT*)? ([eE] SIGN? DIGIT+)?
|
||||
| SIGN? '.' DIGIT+ ([eE] SIGN? DIGIT+)?
|
||||
;
|
||||
|
||||
// Quoted string literals (double or single-quoted, with escape support)
|
||||
QUOTED_TEXT
|
||||
: '"' ( ~["\\] | '\\' . )* '"'
|
||||
| '\'' ( ~['\\] | '\\' . )* '\''
|
||||
;
|
||||
|
||||
// Identifiers: start with a letter or underscore, followed by alphanumeric/underscores.
|
||||
// Optionally dotted for nested field paths.
|
||||
// Covers: count, sum, p99, total_logs, error_count, __result, __result_0, __result0,
|
||||
// service.name, span.duration
|
||||
IDENTIFIER
|
||||
: [a-zA-Z_] [a-zA-Z0-9_]* ( '.' [a-zA-Z_] [a-zA-Z0-9_]* )*
|
||||
;
|
||||
|
||||
// Skip whitespace
|
||||
WS
|
||||
: [ \t\r\n]+ -> skip
|
||||
;
|
||||
|
||||
fragment DIGIT : [0-9] ;
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
parser "github.com/SigNoz/signoz/pkg/parser/grammar"
|
||||
parser "github.com/SigNoz/signoz/pkg/parser/grammar/filterquery"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
package filterquery // FilterQuery
|
||||
|
||||
import "github.com/antlr4-go/antlr/v4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
package filterquery // FilterQuery
|
||||
|
||||
import "github.com/antlr4-go/antlr/v4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser
|
||||
package filterquery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
package filterquery // FilterQuery
|
||||
|
||||
import "github.com/antlr4-go/antlr/v4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
package filterquery // FilterQuery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
package filterquery // FilterQuery
|
||||
|
||||
import "github.com/antlr4-go/antlr/v4"
|
||||
|
||||
72
pkg/parser/grammar/havingexpression/HavingExpression.interp
Normal file
72
pkg/parser/grammar/havingexpression/HavingExpression.interp
Normal file
@@ -0,0 +1,72 @@
|
||||
token literal names:
|
||||
null
|
||||
'('
|
||||
')'
|
||||
','
|
||||
null
|
||||
'!='
|
||||
'<>'
|
||||
'<'
|
||||
'<='
|
||||
'>'
|
||||
'>='
|
||||
'+'
|
||||
'-'
|
||||
'*'
|
||||
'/'
|
||||
'%'
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
|
||||
token symbolic names:
|
||||
null
|
||||
LPAREN
|
||||
RPAREN
|
||||
COMMA
|
||||
EQUALS
|
||||
NOT_EQUALS
|
||||
NEQ
|
||||
LT
|
||||
LE
|
||||
GT
|
||||
GE
|
||||
PLUS
|
||||
MINUS
|
||||
STAR
|
||||
SLASH
|
||||
PERCENT
|
||||
NOT
|
||||
AND
|
||||
OR
|
||||
BOOL
|
||||
NUMBER
|
||||
QUOTED_TEXT
|
||||
IDENTIFIER
|
||||
WS
|
||||
|
||||
rule names:
|
||||
query
|
||||
expression
|
||||
orExpression
|
||||
andExpression
|
||||
primary
|
||||
comparison
|
||||
compOp
|
||||
operand
|
||||
term
|
||||
factor
|
||||
atom
|
||||
functionCall
|
||||
functionArgs
|
||||
functionArg
|
||||
identifier
|
||||
|
||||
|
||||
atn:
|
||||
[4, 1, 23, 121, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 5, 2, 39, 8, 2, 10, 2, 12, 2, 42, 9, 2, 1, 3, 1, 3, 1, 3, 5, 3, 47, 8, 3, 10, 3, 12, 3, 50, 9, 3, 1, 4, 3, 4, 53, 8, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 60, 8, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 74, 8, 7, 10, 7, 12, 7, 77, 9, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 5, 8, 85, 8, 8, 10, 8, 12, 8, 88, 9, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 95, 8, 9, 1, 10, 1, 10, 1, 10, 3, 10, 100, 8, 10, 1, 11, 1, 11, 1, 11, 3, 11, 105, 8, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 5, 12, 112, 8, 12, 10, 12, 12, 12, 115, 9, 12, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 0, 2, 14, 16, 15, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 0, 3, 1, 0, 4, 10, 1, 0, 11, 12, 1, 0, 13, 15, 116, 0, 30, 1, 0, 0, 0, 2, 33, 1, 0, 0, 0, 4, 35, 1, 0, 0, 0, 6, 43, 1, 0, 0, 0, 8, 59, 1, 0, 0, 0, 10, 61, 1, 0, 0, 0, 12, 65, 1, 0, 0, 0, 14, 67, 1, 0, 0, 0, 16, 78, 1, 0, 0, 0, 18, 94, 1, 0, 0, 0, 20, 99, 1, 0, 0, 0, 22, 101, 1, 0, 0, 0, 24, 108, 1, 0, 0, 0, 26, 116, 1, 0, 0, 0, 28, 118, 1, 0, 0, 0, 30, 31, 3, 2, 1, 0, 31, 32, 5, 0, 0, 1, 32, 1, 1, 0, 0, 0, 33, 34, 3, 4, 2, 0, 34, 3, 1, 0, 0, 0, 35, 40, 3, 6, 3, 0, 36, 37, 5, 18, 0, 0, 37, 39, 3, 6, 3, 0, 38, 36, 1, 0, 0, 0, 39, 42, 1, 0, 0, 0, 40, 38, 1, 0, 0, 0, 40, 41, 1, 0, 0, 0, 41, 5, 1, 0, 0, 0, 42, 40, 1, 0, 0, 0, 43, 48, 3, 8, 4, 0, 44, 45, 5, 17, 0, 0, 45, 47, 3, 8, 4, 0, 46, 44, 1, 0, 0, 0, 47, 50, 1, 0, 0, 0, 48, 46, 1, 0, 0, 0, 48, 49, 1, 0, 0, 0, 49, 7, 1, 0, 0, 0, 50, 48, 1, 0, 0, 0, 51, 53, 5, 16, 0, 0, 52, 51, 1, 0, 0, 0, 52, 53, 1, 0, 0, 0, 53, 54, 1, 0, 0, 0, 54, 55, 5, 1, 0, 0, 55, 56, 3, 4, 2, 0, 56, 57, 5, 2, 0, 0, 57, 60, 1, 0, 0, 0, 58, 60, 3, 10, 5, 0, 59, 52, 1, 0, 0, 0, 59, 58, 1, 0, 0, 0, 60, 9, 1, 0, 0, 0, 61, 62, 3, 14, 7, 0, 62, 63, 3, 12, 6, 0, 63, 64, 3, 14, 7, 0, 64, 11, 1, 0, 0, 0, 65, 66, 7, 0, 0, 0, 66, 13, 1, 0, 0, 0, 67, 68, 6, 7, -1, 0, 68, 69, 3, 16, 8, 0, 69, 75, 1, 0, 0, 0, 70, 71, 10, 2, 0, 0, 71, 72, 7, 1, 0, 0, 72, 74, 3, 16, 8, 0, 73, 70, 1, 0, 0, 0, 74, 77, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 75, 76, 1, 0, 0, 0, 76, 15, 1, 0, 0, 0, 77, 75, 1, 0, 0, 0, 78, 79, 6, 8, -1, 0, 79, 80, 3, 18, 9, 0, 80, 86, 1, 0, 0, 0, 81, 82, 10, 2, 0, 0, 82, 83, 7, 2, 0, 0, 83, 85, 3, 18, 9, 0, 84, 81, 1, 0, 0, 0, 85, 88, 1, 0, 0, 0, 86, 84, 1, 0, 0, 0, 86, 87, 1, 0, 0, 0, 87, 17, 1, 0, 0, 0, 88, 86, 1, 0, 0, 0, 89, 90, 5, 1, 0, 0, 90, 91, 3, 14, 7, 0, 91, 92, 5, 2, 0, 0, 92, 95, 1, 0, 0, 0, 93, 95, 3, 20, 10, 0, 94, 89, 1, 0, 0, 0, 94, 93, 1, 0, 0, 0, 95, 19, 1, 0, 0, 0, 96, 100, 3, 22, 11, 0, 97, 100, 3, 28, 14, 0, 98, 100, 5, 20, 0, 0, 99, 96, 1, 0, 0, 0, 99, 97, 1, 0, 0, 0, 99, 98, 1, 0, 0, 0, 100, 21, 1, 0, 0, 0, 101, 102, 5, 22, 0, 0, 102, 104, 5, 1, 0, 0, 103, 105, 3, 24, 12, 0, 104, 103, 1, 0, 0, 0, 104, 105, 1, 0, 0, 0, 105, 106, 1, 0, 0, 0, 106, 107, 5, 2, 0, 0, 107, 23, 1, 0, 0, 0, 108, 113, 3, 26, 13, 0, 109, 110, 5, 3, 0, 0, 110, 112, 3, 26, 13, 0, 111, 109, 1, 0, 0, 0, 112, 115, 1, 0, 0, 0, 113, 111, 1, 0, 0, 0, 113, 114, 1, 0, 0, 0, 114, 25, 1, 0, 0, 0, 115, 113, 1, 0, 0, 0, 116, 117, 5, 22, 0, 0, 117, 27, 1, 0, 0, 0, 118, 119, 5, 22, 0, 0, 119, 29, 1, 0, 0, 0, 10, 40, 48, 52, 59, 75, 86, 94, 99, 104, 113]
|
||||
37
pkg/parser/grammar/havingexpression/HavingExpression.tokens
Normal file
37
pkg/parser/grammar/havingexpression/HavingExpression.tokens
Normal file
@@ -0,0 +1,37 @@
|
||||
LPAREN=1
|
||||
RPAREN=2
|
||||
COMMA=3
|
||||
EQUALS=4
|
||||
NOT_EQUALS=5
|
||||
NEQ=6
|
||||
LT=7
|
||||
LE=8
|
||||
GT=9
|
||||
GE=10
|
||||
PLUS=11
|
||||
MINUS=12
|
||||
STAR=13
|
||||
SLASH=14
|
||||
PERCENT=15
|
||||
NOT=16
|
||||
AND=17
|
||||
OR=18
|
||||
BOOL=19
|
||||
NUMBER=20
|
||||
QUOTED_TEXT=21
|
||||
IDENTIFIER=22
|
||||
WS=23
|
||||
'('=1
|
||||
')'=2
|
||||
','=3
|
||||
'!='=5
|
||||
'<>'=6
|
||||
'<'=7
|
||||
'<='=8
|
||||
'>'=9
|
||||
'>='=10
|
||||
'+'=11
|
||||
'-'=12
|
||||
'*'=13
|
||||
'/'=14
|
||||
'%'=15
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,37 @@
|
||||
LPAREN=1
|
||||
RPAREN=2
|
||||
COMMA=3
|
||||
EQUALS=4
|
||||
NOT_EQUALS=5
|
||||
NEQ=6
|
||||
LT=7
|
||||
LE=8
|
||||
GT=9
|
||||
GE=10
|
||||
PLUS=11
|
||||
MINUS=12
|
||||
STAR=13
|
||||
SLASH=14
|
||||
PERCENT=15
|
||||
NOT=16
|
||||
AND=17
|
||||
OR=18
|
||||
BOOL=19
|
||||
NUMBER=20
|
||||
QUOTED_TEXT=21
|
||||
IDENTIFIER=22
|
||||
WS=23
|
||||
'('=1
|
||||
')'=2
|
||||
','=3
|
||||
'!='=5
|
||||
'<>'=6
|
||||
'<'=7
|
||||
'<='=8
|
||||
'>'=9
|
||||
'>='=10
|
||||
'+'=11
|
||||
'-'=12
|
||||
'*'=13
|
||||
'/'=14
|
||||
'%'=15
|
||||
@@ -0,0 +1,111 @@
|
||||
// Code generated from /Users/tvats/SigNoz/signoz/grammar/HavingExpression.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package havingexpression // HavingExpression
|
||||
import "github.com/antlr4-go/antlr/v4"
|
||||
|
||||
// BaseHavingExpressionListener is a complete listener for a parse tree produced by HavingExpressionParser.
|
||||
type BaseHavingExpressionListener struct{}
|
||||
|
||||
var _ HavingExpressionListener = &BaseHavingExpressionListener{}
|
||||
|
||||
// VisitTerminal is called when a terminal node is visited.
|
||||
func (s *BaseHavingExpressionListener) VisitTerminal(node antlr.TerminalNode) {}
|
||||
|
||||
// VisitErrorNode is called when an error node is visited.
|
||||
func (s *BaseHavingExpressionListener) VisitErrorNode(node antlr.ErrorNode) {}
|
||||
|
||||
// EnterEveryRule is called when any rule is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterEveryRule(ctx antlr.ParserRuleContext) {}
|
||||
|
||||
// ExitEveryRule is called when any rule is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitEveryRule(ctx antlr.ParserRuleContext) {}
|
||||
|
||||
// EnterQuery is called when production query is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterQuery(ctx *QueryContext) {}
|
||||
|
||||
// ExitQuery is called when production query is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitQuery(ctx *QueryContext) {}
|
||||
|
||||
// EnterExpression is called when production expression is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterExpression(ctx *ExpressionContext) {}
|
||||
|
||||
// ExitExpression is called when production expression is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitExpression(ctx *ExpressionContext) {}
|
||||
|
||||
// EnterOrExpression is called when production orExpression is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterOrExpression(ctx *OrExpressionContext) {}
|
||||
|
||||
// ExitOrExpression is called when production orExpression is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitOrExpression(ctx *OrExpressionContext) {}
|
||||
|
||||
// EnterAndExpression is called when production andExpression is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterAndExpression(ctx *AndExpressionContext) {}
|
||||
|
||||
// ExitAndExpression is called when production andExpression is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitAndExpression(ctx *AndExpressionContext) {}
|
||||
|
||||
// EnterPrimary is called when production primary is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterPrimary(ctx *PrimaryContext) {}
|
||||
|
||||
// ExitPrimary is called when production primary is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitPrimary(ctx *PrimaryContext) {}
|
||||
|
||||
// EnterComparison is called when production comparison is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterComparison(ctx *ComparisonContext) {}
|
||||
|
||||
// ExitComparison is called when production comparison is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitComparison(ctx *ComparisonContext) {}
|
||||
|
||||
// EnterCompOp is called when production compOp is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterCompOp(ctx *CompOpContext) {}
|
||||
|
||||
// ExitCompOp is called when production compOp is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitCompOp(ctx *CompOpContext) {}
|
||||
|
||||
// EnterOperand is called when production operand is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterOperand(ctx *OperandContext) {}
|
||||
|
||||
// ExitOperand is called when production operand is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitOperand(ctx *OperandContext) {}
|
||||
|
||||
// EnterTerm is called when production term is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterTerm(ctx *TermContext) {}
|
||||
|
||||
// ExitTerm is called when production term is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitTerm(ctx *TermContext) {}
|
||||
|
||||
// EnterFactor is called when production factor is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterFactor(ctx *FactorContext) {}
|
||||
|
||||
// ExitFactor is called when production factor is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitFactor(ctx *FactorContext) {}
|
||||
|
||||
// EnterAtom is called when production atom is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterAtom(ctx *AtomContext) {}
|
||||
|
||||
// ExitAtom is called when production atom is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitAtom(ctx *AtomContext) {}
|
||||
|
||||
// EnterFunctionCall is called when production functionCall is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterFunctionCall(ctx *FunctionCallContext) {}
|
||||
|
||||
// ExitFunctionCall is called when production functionCall is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitFunctionCall(ctx *FunctionCallContext) {}
|
||||
|
||||
// EnterFunctionArgs is called when production functionArgs is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterFunctionArgs(ctx *FunctionArgsContext) {}
|
||||
|
||||
// ExitFunctionArgs is called when production functionArgs is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitFunctionArgs(ctx *FunctionArgsContext) {}
|
||||
|
||||
// EnterFunctionArg is called when production functionArg is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterFunctionArg(ctx *FunctionArgContext) {}
|
||||
|
||||
// ExitFunctionArg is called when production functionArg is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitFunctionArg(ctx *FunctionArgContext) {}
|
||||
|
||||
// EnterIdentifier is called when production identifier is entered.
|
||||
func (s *BaseHavingExpressionListener) EnterIdentifier(ctx *IdentifierContext) {}
|
||||
|
||||
// ExitIdentifier is called when production identifier is exited.
|
||||
func (s *BaseHavingExpressionListener) ExitIdentifier(ctx *IdentifierContext) {}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Code generated from /Users/tvats/SigNoz/signoz/grammar/HavingExpression.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package havingexpression // HavingExpression
|
||||
import "github.com/antlr4-go/antlr/v4"
|
||||
|
||||
type BaseHavingExpressionVisitor struct {
|
||||
*antlr.BaseParseTreeVisitor
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitQuery(ctx *QueryContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitExpression(ctx *ExpressionContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitOrExpression(ctx *OrExpressionContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitAndExpression(ctx *AndExpressionContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitPrimary(ctx *PrimaryContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitComparison(ctx *ComparisonContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitCompOp(ctx *CompOpContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitOperand(ctx *OperandContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitTerm(ctx *TermContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitFactor(ctx *FactorContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitAtom(ctx *AtomContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitFunctionCall(ctx *FunctionCallContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitFunctionArgs(ctx *FunctionArgsContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitFunctionArg(ctx *FunctionArgContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseHavingExpressionVisitor) VisitIdentifier(ctx *IdentifierContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
227
pkg/parser/grammar/havingexpression/havingexpression_lexer.go
Normal file
227
pkg/parser/grammar/havingexpression/havingexpression_lexer.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// Code generated from /Users/tvats/SigNoz/signoz/grammar/HavingExpression.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package havingexpression
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
"sync"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Suppress unused import error
|
||||
var _ = fmt.Printf
|
||||
var _ = sync.Once{}
|
||||
var _ = unicode.IsLetter
|
||||
|
||||
type HavingExpressionLexer struct {
|
||||
*antlr.BaseLexer
|
||||
channelNames []string
|
||||
modeNames []string
|
||||
// TODO: EOF string
|
||||
}
|
||||
|
||||
var HavingExpressionLexerLexerStaticData struct {
|
||||
once sync.Once
|
||||
serializedATN []int32
|
||||
ChannelNames []string
|
||||
ModeNames []string
|
||||
LiteralNames []string
|
||||
SymbolicNames []string
|
||||
RuleNames []string
|
||||
PredictionContextCache *antlr.PredictionContextCache
|
||||
atn *antlr.ATN
|
||||
decisionToDFA []*antlr.DFA
|
||||
}
|
||||
|
||||
func havingexpressionlexerLexerInit() {
|
||||
staticData := &HavingExpressionLexerLexerStaticData
|
||||
staticData.ChannelNames = []string{
|
||||
"DEFAULT_TOKEN_CHANNEL", "HIDDEN",
|
||||
}
|
||||
staticData.ModeNames = []string{
|
||||
"DEFAULT_MODE",
|
||||
}
|
||||
staticData.LiteralNames = []string{
|
||||
"", "'('", "')'", "','", "", "'!='", "'<>'", "'<'", "'<='", "'>'", "'>='",
|
||||
"'+'", "'-'", "'*'", "'/'", "'%'",
|
||||
}
|
||||
staticData.SymbolicNames = []string{
|
||||
"", "LPAREN", "RPAREN", "COMMA", "EQUALS", "NOT_EQUALS", "NEQ", "LT",
|
||||
"LE", "GT", "GE", "PLUS", "MINUS", "STAR", "SLASH", "PERCENT", "NOT",
|
||||
"AND", "OR", "BOOL", "NUMBER", "QUOTED_TEXT", "IDENTIFIER", "WS",
|
||||
}
|
||||
staticData.RuleNames = []string{
|
||||
"LPAREN", "RPAREN", "COMMA", "EQUALS", "NOT_EQUALS", "NEQ", "LT", "LE",
|
||||
"GT", "GE", "PLUS", "MINUS", "STAR", "SLASH", "PERCENT", "NOT", "AND",
|
||||
"OR", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "IDENTIFIER", "WS", "DIGIT",
|
||||
}
|
||||
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
|
||||
staticData.serializedATN = []int32{
|
||||
4, 0, 23, 213, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
|
||||
4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2,
|
||||
10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15,
|
||||
7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7,
|
||||
20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 1, 0, 1, 0,
|
||||
1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 3, 3, 61, 8, 3, 1, 4, 1, 4, 1,
|
||||
4, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1,
|
||||
9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 1, 14,
|
||||
1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1,
|
||||
17, 1, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18,
|
||||
3, 18, 109, 8, 18, 1, 19, 1, 19, 1, 20, 3, 20, 114, 8, 20, 1, 20, 4, 20,
|
||||
117, 8, 20, 11, 20, 12, 20, 118, 1, 20, 1, 20, 5, 20, 123, 8, 20, 10, 20,
|
||||
12, 20, 126, 9, 20, 3, 20, 128, 8, 20, 1, 20, 1, 20, 3, 20, 132, 8, 20,
|
||||
1, 20, 4, 20, 135, 8, 20, 11, 20, 12, 20, 136, 3, 20, 139, 8, 20, 1, 20,
|
||||
3, 20, 142, 8, 20, 1, 20, 1, 20, 4, 20, 146, 8, 20, 11, 20, 12, 20, 147,
|
||||
1, 20, 1, 20, 3, 20, 152, 8, 20, 1, 20, 4, 20, 155, 8, 20, 11, 20, 12,
|
||||
20, 156, 3, 20, 159, 8, 20, 3, 20, 161, 8, 20, 1, 21, 1, 21, 1, 21, 1,
|
||||
21, 5, 21, 167, 8, 21, 10, 21, 12, 21, 170, 9, 21, 1, 21, 1, 21, 1, 21,
|
||||
1, 21, 1, 21, 5, 21, 177, 8, 21, 10, 21, 12, 21, 180, 9, 21, 1, 21, 3,
|
||||
21, 183, 8, 21, 1, 22, 1, 22, 5, 22, 187, 8, 22, 10, 22, 12, 22, 190, 9,
|
||||
22, 1, 22, 1, 22, 1, 22, 5, 22, 195, 8, 22, 10, 22, 12, 22, 198, 9, 22,
|
||||
5, 22, 200, 8, 22, 10, 22, 12, 22, 203, 9, 22, 1, 23, 4, 23, 206, 8, 23,
|
||||
11, 23, 12, 23, 207, 1, 23, 1, 23, 1, 24, 1, 24, 0, 0, 25, 1, 1, 3, 2,
|
||||
5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25,
|
||||
13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 0, 41, 20, 43,
|
||||
21, 45, 22, 47, 23, 49, 0, 1, 0, 18, 2, 0, 78, 78, 110, 110, 2, 0, 79,
|
||||
79, 111, 111, 2, 0, 84, 84, 116, 116, 2, 0, 65, 65, 97, 97, 2, 0, 68, 68,
|
||||
100, 100, 2, 0, 82, 82, 114, 114, 2, 0, 85, 85, 117, 117, 2, 0, 69, 69,
|
||||
101, 101, 2, 0, 70, 70, 102, 102, 2, 0, 76, 76, 108, 108, 2, 0, 83, 83,
|
||||
115, 115, 2, 0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92,
|
||||
92, 3, 0, 65, 90, 95, 95, 97, 122, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122,
|
||||
3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 234, 0, 1, 1, 0, 0, 0, 0, 3,
|
||||
1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11,
|
||||
1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0,
|
||||
19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0,
|
||||
0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0,
|
||||
0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0,
|
||||
0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 1, 51, 1, 0, 0, 0, 3, 53, 1,
|
||||
0, 0, 0, 5, 55, 1, 0, 0, 0, 7, 60, 1, 0, 0, 0, 9, 62, 1, 0, 0, 0, 11, 65,
|
||||
1, 0, 0, 0, 13, 68, 1, 0, 0, 0, 15, 70, 1, 0, 0, 0, 17, 73, 1, 0, 0, 0,
|
||||
19, 75, 1, 0, 0, 0, 21, 78, 1, 0, 0, 0, 23, 80, 1, 0, 0, 0, 25, 82, 1,
|
||||
0, 0, 0, 27, 84, 1, 0, 0, 0, 29, 86, 1, 0, 0, 0, 31, 88, 1, 0, 0, 0, 33,
|
||||
92, 1, 0, 0, 0, 35, 96, 1, 0, 0, 0, 37, 108, 1, 0, 0, 0, 39, 110, 1, 0,
|
||||
0, 0, 41, 160, 1, 0, 0, 0, 43, 182, 1, 0, 0, 0, 45, 184, 1, 0, 0, 0, 47,
|
||||
205, 1, 0, 0, 0, 49, 211, 1, 0, 0, 0, 51, 52, 5, 40, 0, 0, 52, 2, 1, 0,
|
||||
0, 0, 53, 54, 5, 41, 0, 0, 54, 4, 1, 0, 0, 0, 55, 56, 5, 44, 0, 0, 56,
|
||||
6, 1, 0, 0, 0, 57, 61, 5, 61, 0, 0, 58, 59, 5, 61, 0, 0, 59, 61, 5, 61,
|
||||
0, 0, 60, 57, 1, 0, 0, 0, 60, 58, 1, 0, 0, 0, 61, 8, 1, 0, 0, 0, 62, 63,
|
||||
5, 33, 0, 0, 63, 64, 5, 61, 0, 0, 64, 10, 1, 0, 0, 0, 65, 66, 5, 60, 0,
|
||||
0, 66, 67, 5, 62, 0, 0, 67, 12, 1, 0, 0, 0, 68, 69, 5, 60, 0, 0, 69, 14,
|
||||
1, 0, 0, 0, 70, 71, 5, 60, 0, 0, 71, 72, 5, 61, 0, 0, 72, 16, 1, 0, 0,
|
||||
0, 73, 74, 5, 62, 0, 0, 74, 18, 1, 0, 0, 0, 75, 76, 5, 62, 0, 0, 76, 77,
|
||||
5, 61, 0, 0, 77, 20, 1, 0, 0, 0, 78, 79, 5, 43, 0, 0, 79, 22, 1, 0, 0,
|
||||
0, 80, 81, 5, 45, 0, 0, 81, 24, 1, 0, 0, 0, 82, 83, 5, 42, 0, 0, 83, 26,
|
||||
1, 0, 0, 0, 84, 85, 5, 47, 0, 0, 85, 28, 1, 0, 0, 0, 86, 87, 5, 37, 0,
|
||||
0, 87, 30, 1, 0, 0, 0, 88, 89, 7, 0, 0, 0, 89, 90, 7, 1, 0, 0, 90, 91,
|
||||
7, 2, 0, 0, 91, 32, 1, 0, 0, 0, 92, 93, 7, 3, 0, 0, 93, 94, 7, 0, 0, 0,
|
||||
94, 95, 7, 4, 0, 0, 95, 34, 1, 0, 0, 0, 96, 97, 7, 1, 0, 0, 97, 98, 7,
|
||||
5, 0, 0, 98, 36, 1, 0, 0, 0, 99, 100, 7, 2, 0, 0, 100, 101, 7, 5, 0, 0,
|
||||
101, 102, 7, 6, 0, 0, 102, 109, 7, 7, 0, 0, 103, 104, 7, 8, 0, 0, 104,
|
||||
105, 7, 3, 0, 0, 105, 106, 7, 9, 0, 0, 106, 107, 7, 10, 0, 0, 107, 109,
|
||||
7, 7, 0, 0, 108, 99, 1, 0, 0, 0, 108, 103, 1, 0, 0, 0, 109, 38, 1, 0, 0,
|
||||
0, 110, 111, 7, 11, 0, 0, 111, 40, 1, 0, 0, 0, 112, 114, 3, 39, 19, 0,
|
||||
113, 112, 1, 0, 0, 0, 113, 114, 1, 0, 0, 0, 114, 116, 1, 0, 0, 0, 115,
|
||||
117, 3, 49, 24, 0, 116, 115, 1, 0, 0, 0, 117, 118, 1, 0, 0, 0, 118, 116,
|
||||
1, 0, 0, 0, 118, 119, 1, 0, 0, 0, 119, 127, 1, 0, 0, 0, 120, 124, 5, 46,
|
||||
0, 0, 121, 123, 3, 49, 24, 0, 122, 121, 1, 0, 0, 0, 123, 126, 1, 0, 0,
|
||||
0, 124, 122, 1, 0, 0, 0, 124, 125, 1, 0, 0, 0, 125, 128, 1, 0, 0, 0, 126,
|
||||
124, 1, 0, 0, 0, 127, 120, 1, 0, 0, 0, 127, 128, 1, 0, 0, 0, 128, 138,
|
||||
1, 0, 0, 0, 129, 131, 7, 7, 0, 0, 130, 132, 3, 39, 19, 0, 131, 130, 1,
|
||||
0, 0, 0, 131, 132, 1, 0, 0, 0, 132, 134, 1, 0, 0, 0, 133, 135, 3, 49, 24,
|
||||
0, 134, 133, 1, 0, 0, 0, 135, 136, 1, 0, 0, 0, 136, 134, 1, 0, 0, 0, 136,
|
||||
137, 1, 0, 0, 0, 137, 139, 1, 0, 0, 0, 138, 129, 1, 0, 0, 0, 138, 139,
|
||||
1, 0, 0, 0, 139, 161, 1, 0, 0, 0, 140, 142, 3, 39, 19, 0, 141, 140, 1,
|
||||
0, 0, 0, 141, 142, 1, 0, 0, 0, 142, 143, 1, 0, 0, 0, 143, 145, 5, 46, 0,
|
||||
0, 144, 146, 3, 49, 24, 0, 145, 144, 1, 0, 0, 0, 146, 147, 1, 0, 0, 0,
|
||||
147, 145, 1, 0, 0, 0, 147, 148, 1, 0, 0, 0, 148, 158, 1, 0, 0, 0, 149,
|
||||
151, 7, 7, 0, 0, 150, 152, 3, 39, 19, 0, 151, 150, 1, 0, 0, 0, 151, 152,
|
||||
1, 0, 0, 0, 152, 154, 1, 0, 0, 0, 153, 155, 3, 49, 24, 0, 154, 153, 1,
|
||||
0, 0, 0, 155, 156, 1, 0, 0, 0, 156, 154, 1, 0, 0, 0, 156, 157, 1, 0, 0,
|
||||
0, 157, 159, 1, 0, 0, 0, 158, 149, 1, 0, 0, 0, 158, 159, 1, 0, 0, 0, 159,
|
||||
161, 1, 0, 0, 0, 160, 113, 1, 0, 0, 0, 160, 141, 1, 0, 0, 0, 161, 42, 1,
|
||||
0, 0, 0, 162, 168, 5, 34, 0, 0, 163, 167, 8, 12, 0, 0, 164, 165, 5, 92,
|
||||
0, 0, 165, 167, 9, 0, 0, 0, 166, 163, 1, 0, 0, 0, 166, 164, 1, 0, 0, 0,
|
||||
167, 170, 1, 0, 0, 0, 168, 166, 1, 0, 0, 0, 168, 169, 1, 0, 0, 0, 169,
|
||||
171, 1, 0, 0, 0, 170, 168, 1, 0, 0, 0, 171, 183, 5, 34, 0, 0, 172, 178,
|
||||
5, 39, 0, 0, 173, 177, 8, 13, 0, 0, 174, 175, 5, 92, 0, 0, 175, 177, 9,
|
||||
0, 0, 0, 176, 173, 1, 0, 0, 0, 176, 174, 1, 0, 0, 0, 177, 180, 1, 0, 0,
|
||||
0, 178, 176, 1, 0, 0, 0, 178, 179, 1, 0, 0, 0, 179, 181, 1, 0, 0, 0, 180,
|
||||
178, 1, 0, 0, 0, 181, 183, 5, 39, 0, 0, 182, 162, 1, 0, 0, 0, 182, 172,
|
||||
1, 0, 0, 0, 183, 44, 1, 0, 0, 0, 184, 188, 7, 14, 0, 0, 185, 187, 7, 15,
|
||||
0, 0, 186, 185, 1, 0, 0, 0, 187, 190, 1, 0, 0, 0, 188, 186, 1, 0, 0, 0,
|
||||
188, 189, 1, 0, 0, 0, 189, 201, 1, 0, 0, 0, 190, 188, 1, 0, 0, 0, 191,
|
||||
192, 5, 46, 0, 0, 192, 196, 7, 14, 0, 0, 193, 195, 7, 15, 0, 0, 194, 193,
|
||||
1, 0, 0, 0, 195, 198, 1, 0, 0, 0, 196, 194, 1, 0, 0, 0, 196, 197, 1, 0,
|
||||
0, 0, 197, 200, 1, 0, 0, 0, 198, 196, 1, 0, 0, 0, 199, 191, 1, 0, 0, 0,
|
||||
200, 203, 1, 0, 0, 0, 201, 199, 1, 0, 0, 0, 201, 202, 1, 0, 0, 0, 202,
|
||||
46, 1, 0, 0, 0, 203, 201, 1, 0, 0, 0, 204, 206, 7, 16, 0, 0, 205, 204,
|
||||
1, 0, 0, 0, 206, 207, 1, 0, 0, 0, 207, 205, 1, 0, 0, 0, 207, 208, 1, 0,
|
||||
0, 0, 208, 209, 1, 0, 0, 0, 209, 210, 6, 23, 0, 0, 210, 48, 1, 0, 0, 0,
|
||||
211, 212, 7, 17, 0, 0, 212, 50, 1, 0, 0, 0, 25, 0, 60, 108, 113, 118, 124,
|
||||
127, 131, 136, 138, 141, 147, 151, 156, 158, 160, 166, 168, 176, 178, 182,
|
||||
188, 196, 201, 207, 1, 6, 0, 0,
|
||||
}
|
||||
deserializer := antlr.NewATNDeserializer(nil)
|
||||
staticData.atn = deserializer.Deserialize(staticData.serializedATN)
|
||||
atn := staticData.atn
|
||||
staticData.decisionToDFA = make([]*antlr.DFA, len(atn.DecisionToState))
|
||||
decisionToDFA := staticData.decisionToDFA
|
||||
for index, state := range atn.DecisionToState {
|
||||
decisionToDFA[index] = antlr.NewDFA(state, index)
|
||||
}
|
||||
}
|
||||
|
||||
// HavingExpressionLexerInit initializes any static state used to implement HavingExpressionLexer. By default the
|
||||
// static state used to implement the lexer is lazily initialized during the first call to
|
||||
// NewHavingExpressionLexer(). You can call this function if you wish to initialize the static state ahead
|
||||
// of time.
|
||||
func HavingExpressionLexerInit() {
|
||||
staticData := &HavingExpressionLexerLexerStaticData
|
||||
staticData.once.Do(havingexpressionlexerLexerInit)
|
||||
}
|
||||
|
||||
// NewHavingExpressionLexer produces a new lexer instance for the optional input antlr.CharStream.
|
||||
func NewHavingExpressionLexer(input antlr.CharStream) *HavingExpressionLexer {
|
||||
HavingExpressionLexerInit()
|
||||
l := new(HavingExpressionLexer)
|
||||
l.BaseLexer = antlr.NewBaseLexer(input)
|
||||
staticData := &HavingExpressionLexerLexerStaticData
|
||||
l.Interpreter = antlr.NewLexerATNSimulator(l, staticData.atn, staticData.decisionToDFA, staticData.PredictionContextCache)
|
||||
l.channelNames = staticData.ChannelNames
|
||||
l.modeNames = staticData.ModeNames
|
||||
l.RuleNames = staticData.RuleNames
|
||||
l.LiteralNames = staticData.LiteralNames
|
||||
l.SymbolicNames = staticData.SymbolicNames
|
||||
l.GrammarFileName = "HavingExpression.g4"
|
||||
// TODO: l.EOF = antlr.TokenEOF
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// HavingExpressionLexer tokens.
|
||||
const (
|
||||
HavingExpressionLexerLPAREN = 1
|
||||
HavingExpressionLexerRPAREN = 2
|
||||
HavingExpressionLexerCOMMA = 3
|
||||
HavingExpressionLexerEQUALS = 4
|
||||
HavingExpressionLexerNOT_EQUALS = 5
|
||||
HavingExpressionLexerNEQ = 6
|
||||
HavingExpressionLexerLT = 7
|
||||
HavingExpressionLexerLE = 8
|
||||
HavingExpressionLexerGT = 9
|
||||
HavingExpressionLexerGE = 10
|
||||
HavingExpressionLexerPLUS = 11
|
||||
HavingExpressionLexerMINUS = 12
|
||||
HavingExpressionLexerSTAR = 13
|
||||
HavingExpressionLexerSLASH = 14
|
||||
HavingExpressionLexerPERCENT = 15
|
||||
HavingExpressionLexerNOT = 16
|
||||
HavingExpressionLexerAND = 17
|
||||
HavingExpressionLexerOR = 18
|
||||
HavingExpressionLexerBOOL = 19
|
||||
HavingExpressionLexerNUMBER = 20
|
||||
HavingExpressionLexerQUOTED_TEXT = 21
|
||||
HavingExpressionLexerIDENTIFIER = 22
|
||||
HavingExpressionLexerWS = 23
|
||||
)
|
||||
@@ -0,0 +1,99 @@
|
||||
// Code generated from /Users/tvats/SigNoz/signoz/grammar/HavingExpression.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package havingexpression // HavingExpression
|
||||
import "github.com/antlr4-go/antlr/v4"
|
||||
|
||||
// HavingExpressionListener is a complete listener for a parse tree produced by HavingExpressionParser.
|
||||
type HavingExpressionListener interface {
|
||||
antlr.ParseTreeListener
|
||||
|
||||
// EnterQuery is called when entering the query production.
|
||||
EnterQuery(c *QueryContext)
|
||||
|
||||
// EnterExpression is called when entering the expression production.
|
||||
EnterExpression(c *ExpressionContext)
|
||||
|
||||
// EnterOrExpression is called when entering the orExpression production.
|
||||
EnterOrExpression(c *OrExpressionContext)
|
||||
|
||||
// EnterAndExpression is called when entering the andExpression production.
|
||||
EnterAndExpression(c *AndExpressionContext)
|
||||
|
||||
// EnterPrimary is called when entering the primary production.
|
||||
EnterPrimary(c *PrimaryContext)
|
||||
|
||||
// EnterComparison is called when entering the comparison production.
|
||||
EnterComparison(c *ComparisonContext)
|
||||
|
||||
// EnterCompOp is called when entering the compOp production.
|
||||
EnterCompOp(c *CompOpContext)
|
||||
|
||||
// EnterOperand is called when entering the operand production.
|
||||
EnterOperand(c *OperandContext)
|
||||
|
||||
// EnterTerm is called when entering the term production.
|
||||
EnterTerm(c *TermContext)
|
||||
|
||||
// EnterFactor is called when entering the factor production.
|
||||
EnterFactor(c *FactorContext)
|
||||
|
||||
// EnterAtom is called when entering the atom production.
|
||||
EnterAtom(c *AtomContext)
|
||||
|
||||
// EnterFunctionCall is called when entering the functionCall production.
|
||||
EnterFunctionCall(c *FunctionCallContext)
|
||||
|
||||
// EnterFunctionArgs is called when entering the functionArgs production.
|
||||
EnterFunctionArgs(c *FunctionArgsContext)
|
||||
|
||||
// EnterFunctionArg is called when entering the functionArg production.
|
||||
EnterFunctionArg(c *FunctionArgContext)
|
||||
|
||||
// EnterIdentifier is called when entering the identifier production.
|
||||
EnterIdentifier(c *IdentifierContext)
|
||||
|
||||
// ExitQuery is called when exiting the query production.
|
||||
ExitQuery(c *QueryContext)
|
||||
|
||||
// ExitExpression is called when exiting the expression production.
|
||||
ExitExpression(c *ExpressionContext)
|
||||
|
||||
// ExitOrExpression is called when exiting the orExpression production.
|
||||
ExitOrExpression(c *OrExpressionContext)
|
||||
|
||||
// ExitAndExpression is called when exiting the andExpression production.
|
||||
ExitAndExpression(c *AndExpressionContext)
|
||||
|
||||
// ExitPrimary is called when exiting the primary production.
|
||||
ExitPrimary(c *PrimaryContext)
|
||||
|
||||
// ExitComparison is called when exiting the comparison production.
|
||||
ExitComparison(c *ComparisonContext)
|
||||
|
||||
// ExitCompOp is called when exiting the compOp production.
|
||||
ExitCompOp(c *CompOpContext)
|
||||
|
||||
// ExitOperand is called when exiting the operand production.
|
||||
ExitOperand(c *OperandContext)
|
||||
|
||||
// ExitTerm is called when exiting the term production.
|
||||
ExitTerm(c *TermContext)
|
||||
|
||||
// ExitFactor is called when exiting the factor production.
|
||||
ExitFactor(c *FactorContext)
|
||||
|
||||
// ExitAtom is called when exiting the atom production.
|
||||
ExitAtom(c *AtomContext)
|
||||
|
||||
// ExitFunctionCall is called when exiting the functionCall production.
|
||||
ExitFunctionCall(c *FunctionCallContext)
|
||||
|
||||
// ExitFunctionArgs is called when exiting the functionArgs production.
|
||||
ExitFunctionArgs(c *FunctionArgsContext)
|
||||
|
||||
// ExitFunctionArg is called when exiting the functionArg production.
|
||||
ExitFunctionArg(c *FunctionArgContext)
|
||||
|
||||
// ExitIdentifier is called when exiting the identifier production.
|
||||
ExitIdentifier(c *IdentifierContext)
|
||||
}
|
||||
2646
pkg/parser/grammar/havingexpression/havingexpression_parser.go
Normal file
2646
pkg/parser/grammar/havingexpression/havingexpression_parser.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
// Code generated from /Users/tvats/SigNoz/signoz/grammar/HavingExpression.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package havingexpression // HavingExpression
|
||||
import "github.com/antlr4-go/antlr/v4"
|
||||
|
||||
// A complete Visitor for a parse tree produced by HavingExpressionParser.
|
||||
type HavingExpressionVisitor interface {
|
||||
antlr.ParseTreeVisitor
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#query.
|
||||
VisitQuery(ctx *QueryContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#expression.
|
||||
VisitExpression(ctx *ExpressionContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#orExpression.
|
||||
VisitOrExpression(ctx *OrExpressionContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#andExpression.
|
||||
VisitAndExpression(ctx *AndExpressionContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#primary.
|
||||
VisitPrimary(ctx *PrimaryContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#comparison.
|
||||
VisitComparison(ctx *ComparisonContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#compOp.
|
||||
VisitCompOp(ctx *CompOpContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#operand.
|
||||
VisitOperand(ctx *OperandContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#term.
|
||||
VisitTerm(ctx *TermContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#factor.
|
||||
VisitFactor(ctx *FactorContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#atom.
|
||||
VisitAtom(ctx *AtomContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#functionCall.
|
||||
VisitFunctionCall(ctx *FunctionCallContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#functionArgs.
|
||||
VisitFunctionArgs(ctx *FunctionArgsContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#functionArg.
|
||||
VisitFunctionArg(ctx *FunctionArgContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by HavingExpressionParser#identifier.
|
||||
VisitIdentifier(ctx *IdentifierContext) interface{}
|
||||
}
|
||||
201
pkg/querybuilder/having_expression_validator.go
Normal file
201
pkg/querybuilder/having_expression_validator.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar/havingexpression"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
// havingExpressionSemanticValidator walks a rewritten HavingExpression parse tree and
|
||||
// validates that every remaining identifier or function-call token is a known SQL column name.
|
||||
//
|
||||
// By the time this validator runs, rewriteExpression has already replaced all valid
|
||||
// user-facing references (aliases, expression strings, __result_N, etc.) with their
|
||||
// corresponding SQL column names (e.g. __result_0, value). Any identifier or function
|
||||
// call that survived the rewrite was not in the column map and is therefore invalid.
|
||||
//
|
||||
// Validation strategy:
|
||||
// - IdentifierContext nodes are checked against validColumns (the SQL column name set).
|
||||
// - FunctionCallContext nodes are always invalid after rewriting (valid calls were
|
||||
// already replaced); their component tokens are reported as unknown.
|
||||
// - FunctionArg children are not visited individually — they are part of a function
|
||||
// call unit and are only reported when the whole call is rejected.
|
||||
type havingExpressionSemanticValidator struct {
|
||||
validColumns map[string]bool // SQL column names (the TO side of columnMap)
|
||||
invalid []string // tokens not recognised as valid SQL columns
|
||||
seen map[string]bool // deduplication
|
||||
}
|
||||
|
||||
func newHavingExpressionSemanticValidator(validColumns map[string]bool) *havingExpressionSemanticValidator {
|
||||
return &havingExpressionSemanticValidator{
|
||||
validColumns: validColumns,
|
||||
seen: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Visit dispatches on parse-tree node type. Unrecognised nodes recurse into their children.
|
||||
func (v *havingExpressionSemanticValidator) Visit(tree antlr.ParseTree) {
|
||||
switch t := tree.(type) {
|
||||
case *grammar.FunctionCallContext:
|
||||
// Validate as a single unit; do NOT recurse further so that function-arg
|
||||
// IDENTIFIERs are not checked individually.
|
||||
v.visitFunctionCall(t)
|
||||
case *grammar.IdentifierContext:
|
||||
v.visitIdentifier(t)
|
||||
default:
|
||||
for i := 0; i < tree.GetChildCount(); i++ {
|
||||
if child, ok := tree.GetChild(i).(antlr.ParseTree); ok {
|
||||
v.Visit(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// visitFunctionCall reports any remaining function call as invalid.
|
||||
// After rewriting, all valid function-call references (e.g. "sum(bytes)") have
|
||||
// already been replaced with SQL column names, so any function call seen here
|
||||
// was not in the column map. Report the function name as invalid; for args,
|
||||
// only report those that are not valid column references (e.g. sum(__result_0)
|
||||
// should report only "sum", since __result_0 is a valid column).
|
||||
func (v *havingExpressionSemanticValidator) visitFunctionCall(ctx *grammar.FunctionCallContext) {
|
||||
funcName := ctx.IDENTIFIER().GetText()
|
||||
|
||||
if !v.seen[funcName] {
|
||||
v.invalid = append(v.invalid, funcName)
|
||||
v.seen[funcName] = true
|
||||
}
|
||||
|
||||
if ctx.FunctionArgs() != nil {
|
||||
for _, arg := range ctx.FunctionArgs().AllFunctionArg() {
|
||||
argText := arg.IDENTIFIER().GetText()
|
||||
// Only report args that are not valid column references
|
||||
if v.validColumns[argText] {
|
||||
continue
|
||||
}
|
||||
if !v.seen[argText] {
|
||||
v.invalid = append(v.invalid, argText)
|
||||
v.seen[argText] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// visitIdentifier checks that a bare identifier is a known SQL column name.
|
||||
// After rewriting, valid references have been replaced (e.g. alias → __result_0),
|
||||
// so any identifier not in validColumns was not a recognised reference.
|
||||
func (v *havingExpressionSemanticValidator) visitIdentifier(ctx *grammar.IdentifierContext) {
|
||||
name := ctx.IDENTIFIER().GetText()
|
||||
if v.validColumns[name] {
|
||||
return
|
||||
}
|
||||
if !v.seen[name] {
|
||||
v.invalid = append(v.invalid, name)
|
||||
v.seen[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// validateWithANTLR parses the HAVING expression with the generated ANTLR lexer/parser
|
||||
// and then performs semantic validation via havingExpressionSemanticValidator.
|
||||
//
|
||||
// It must be called BEFORE rewriteExpression so the expression still contains
|
||||
// user-facing references (aliases, function-call strings, __result_N, etc.).
|
||||
//
|
||||
// Validation layers:
|
||||
// 1. Quoted string literals are detected early in the token stream (QUOTED_TEXT token
|
||||
// is intentionally excluded from the grammar's atom rule) → descriptive error.
|
||||
// 2. ANTLR syntax errors (unbalanced parentheses, missing comparison operators,
|
||||
// dangling AND/OR, etc.) → syntax error wrapping the ANTLR message.
|
||||
// 3. Semantic errors (unknown identifiers / function calls) → lists the offending tokens.
|
||||
func (r *HavingExpressionRewriter) validateWithANTLR(expression string) error {
|
||||
input := antlr.NewInputStream(expression)
|
||||
lexer := grammar.NewHavingExpressionLexer(input)
|
||||
|
||||
lexerErrListener := NewErrorListener()
|
||||
lexer.RemoveErrorListeners()
|
||||
lexer.AddErrorListener(lexerErrListener)
|
||||
|
||||
tokens := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel)
|
||||
p := grammar.NewHavingExpressionParser(tokens)
|
||||
|
||||
parserErrListener := NewErrorListener()
|
||||
p.RemoveErrorListeners()
|
||||
p.AddErrorListener(parserErrListener)
|
||||
|
||||
// Layer 1 – Reject quoted string literals before parsing.
|
||||
// HAVING expressions compare numeric aggregate results; string literals are not valid.
|
||||
for _, ch := range expression {
|
||||
if ch == '\'' || ch == '"' {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"HAVING expression cannot contain string literals; aggregate results are numeric",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Build the parse tree.
|
||||
tree := p.Query()
|
||||
|
||||
// Layer 2 – ANTLR syntax errors.
|
||||
allSyntaxErrors := append(lexerErrListener.SyntaxErrors, parserErrListener.SyntaxErrors...)
|
||||
if len(allSyntaxErrors) > 0 {
|
||||
msgs := make([]string, 0, len(allSyntaxErrors))
|
||||
for _, se := range allSyntaxErrors {
|
||||
if m := se.Error(); m != "" {
|
||||
msgs = append(msgs, m)
|
||||
}
|
||||
}
|
||||
detail := strings.Join(msgs, "; ")
|
||||
if detail == "" {
|
||||
detail = "check the expression syntax"
|
||||
}
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"syntax error in HAVING expression: %s",
|
||||
detail,
|
||||
)
|
||||
}
|
||||
|
||||
// Layer 3 – Semantic validation: every remaining identifier must be a known
|
||||
// SQL column name. Build validColumns from the TO side of columnMap.
|
||||
validColumns := make(map[string]bool, len(r.columnMap))
|
||||
for _, col := range r.columnMap {
|
||||
validColumns[col] = true
|
||||
}
|
||||
sv := newHavingExpressionSemanticValidator(validColumns)
|
||||
sv.Visit(tree)
|
||||
|
||||
if len(sv.invalid) > 0 {
|
||||
sort.Strings(sv.invalid)
|
||||
hasAggFunc := false
|
||||
for _, ref := range sv.invalid {
|
||||
if _, isAgg := AggreFuncMap[valuer.NewString(ref)]; isAgg {
|
||||
hasAggFunc = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasAggFunc {
|
||||
// At least one invalid ref is an aggregation function — use tailored message
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"aggregation functions are not allowed in HAVING expression",
|
||||
)
|
||||
}
|
||||
validKeys := make([]string, 0, len(r.columnMap))
|
||||
for k := range r.columnMap {
|
||||
validKeys = append(validKeys, k)
|
||||
}
|
||||
sort.Strings(validKeys)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid references in HAVING expression: [%s]. Valid references are: [%s]",
|
||||
strings.Join(sv.invalid, ", "),
|
||||
strings.Join(validKeys, ", "),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -19,19 +19,22 @@ func NewHavingExpressionRewriter() *HavingExpressionRewriter {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *HavingExpressionRewriter) RewriteForTraces(expression string, aggregations []qbtypes.TraceAggregation) string {
|
||||
func (r *HavingExpressionRewriter) RewriteForTraces(expression string, aggregations []qbtypes.TraceAggregation) (string, error) {
|
||||
r.buildTraceColumnMap(aggregations)
|
||||
return r.rewriteExpression(expression)
|
||||
expression = r.rewriteExpression(expression)
|
||||
return expression, r.validateWithANTLR(expression)
|
||||
}
|
||||
|
||||
func (r *HavingExpressionRewriter) RewriteForLogs(expression string, aggregations []qbtypes.LogAggregation) string {
|
||||
func (r *HavingExpressionRewriter) RewriteForLogs(expression string, aggregations []qbtypes.LogAggregation) (string, error) {
|
||||
r.buildLogColumnMap(aggregations)
|
||||
return r.rewriteExpression(expression)
|
||||
expression = r.rewriteExpression(expression)
|
||||
return expression, r.validateWithANTLR(expression)
|
||||
}
|
||||
|
||||
func (r *HavingExpressionRewriter) RewriteForMetrics(expression string, aggregations []qbtypes.MetricAggregation) string {
|
||||
func (r *HavingExpressionRewriter) RewriteForMetrics(expression string, aggregations []qbtypes.MetricAggregation) (string, error) {
|
||||
r.buildMetricColumnMap(aggregations)
|
||||
return r.rewriteExpression(expression)
|
||||
expression = r.rewriteExpression(expression)
|
||||
return expression, r.validateWithANTLR(expression)
|
||||
}
|
||||
|
||||
func (r *HavingExpressionRewriter) buildTraceColumnMap(aggregations []qbtypes.TraceAggregation) {
|
||||
@@ -151,3 +154,4 @@ func (r *HavingExpressionRewriter) rewriteExpression(expression string) string {
|
||||
|
||||
return expression
|
||||
}
|
||||
|
||||
|
||||
708
pkg/querybuilder/having_rewriter_test.go
Normal file
708
pkg/querybuilder/having_rewriter_test.go
Normal file
@@ -0,0 +1,708 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func toTraceAggregations(logs []qbtypes.LogAggregation) []qbtypes.TraceAggregation {
|
||||
out := make([]qbtypes.TraceAggregation, len(logs))
|
||||
for i, l := range logs {
|
||||
out[i] = qbtypes.TraceAggregation{Expression: l.Expression, Alias: l.Alias}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestRewriteForLogsAndTraces(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
aggregations []qbtypes.LogAggregation
|
||||
wantExpression string
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
// --- Happy path: reference types (alias, expression, __result variants) ---
|
||||
{
|
||||
name: "alias reference",
|
||||
expression: "total_logs > 1000",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantExpression: "__result_0 > 1000",
|
||||
},
|
||||
{
|
||||
name: "expression reference",
|
||||
expression: "sum(bytes) > 1024000",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "sum(bytes)"},
|
||||
},
|
||||
wantExpression: "__result_0 > 1024000",
|
||||
},
|
||||
{
|
||||
name: "__result reference for single aggregation",
|
||||
expression: "__result > 500",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantExpression: "__result_0 > 500",
|
||||
},
|
||||
{
|
||||
name: "__result0 indexed reference",
|
||||
expression: "__result0 > 100 AND __result1 < 1000",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
{Expression: "sum(bytes)"},
|
||||
},
|
||||
wantExpression: "__result_0 > 100 AND __result_1 < 1000",
|
||||
},
|
||||
{
|
||||
name: "__result_0 underscore indexed reference",
|
||||
expression: "__result_0 > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantExpression: "__result_0 > 100",
|
||||
},
|
||||
// --- Happy path: boolean logic (AND, OR, NOT, parentheses) ---
|
||||
{
|
||||
name: "complex boolean with parentheses",
|
||||
expression: "(total > 100 AND avg_duration < 500) OR total > 10000",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
{Expression: "avg(duration)", Alias: "avg_duration"},
|
||||
},
|
||||
wantExpression: "(__result_0 > 100 AND __result_1 < 500) OR __result_0 > 10000",
|
||||
},
|
||||
{
|
||||
name: "mixed alias and expression reference",
|
||||
expression: "error_count > 10 AND count() < 1000",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
{Expression: "countIf(level='error')", Alias: "error_count"},
|
||||
},
|
||||
wantExpression: "__result_1 > 10 AND __result_0 < 1000",
|
||||
},
|
||||
{
|
||||
name: "NOT on grouped expression",
|
||||
expression: "NOT (__result_0 > 100 AND __result_1 < 500)",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
{Expression: "sum(bytes)"},
|
||||
},
|
||||
wantExpression: "NOT (__result_0 > 100 AND __result_1 < 500)",
|
||||
},
|
||||
// --- Happy path: arithmetic operators (+, -, *, /, %) ---
|
||||
{
|
||||
name: "arithmetic on aggregations",
|
||||
expression: "sum_a + sum_b > 1000",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "sum(a)", Alias: "sum_a"},
|
||||
{Expression: "sum(b)", Alias: "sum_b"},
|
||||
},
|
||||
wantExpression: "__result_0 + __result_1 > 1000",
|
||||
},
|
||||
// --- Happy path: comparison operators (=, ==, !=, <>, <, <=, >, >=) ---
|
||||
{
|
||||
name: "comparison operators != <> ==",
|
||||
expression: "total != 0 AND count() <> 0 AND total == 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantExpression: "__result_0 != 0 AND __result_0 <> 0 AND __result_0 == 100",
|
||||
},
|
||||
{
|
||||
name: "comparison operators < <= > >=",
|
||||
expression: "total < 100 AND total <= 500 AND total > 10 AND total >= 50",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantExpression: "__result_0 < 100 AND __result_0 <= 500 AND __result_0 > 10 AND __result_0 >= 50",
|
||||
},
|
||||
// --- Happy path: numeric literals (negative, float, scientific notation) ---
|
||||
{
|
||||
name: "numeric literals",
|
||||
expression: "total > -10 AND total > 500.5 AND total > 1e6",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantExpression: "__result_0 > -10 AND __result_0 > 500.5 AND __result_0 > 1e6",
|
||||
},
|
||||
{
|
||||
name: "arithmetic modulo subtraction division multiplication",
|
||||
expression: "cnt % 10 = 0 AND cnt - 10 > 0 AND errors / total > 0.05 AND cnt * 2 > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "cnt"},
|
||||
{Expression: "sum(errors)", Alias: "errors"},
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantExpression: "__result_0 % 10 = 0 AND __result_0 - 10 > 0 AND __result_1 / __result_2 > 0.05 AND __result_0 * 2 > 100",
|
||||
},
|
||||
// --- Happy path: OR with multiple operands ---
|
||||
{
|
||||
name: "OR with three operands",
|
||||
expression: "a > 1 OR b > 2 OR c > 3",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "a"},
|
||||
{Expression: "count()", Alias: "b"},
|
||||
{Expression: "count()", Alias: "c"},
|
||||
},
|
||||
wantExpression: "__result_0 > 1 OR __result_1 > 2 OR __result_2 > 3",
|
||||
},
|
||||
{
|
||||
name: "NOT with single comparison",
|
||||
expression: "NOT (total > 100)",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantExpression: "NOT (__result_0 > 100)",
|
||||
},
|
||||
{
|
||||
name: "nested parentheses with OR and AND",
|
||||
expression: "(a > 10 OR b > 20) AND c > 5",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "a"},
|
||||
{Expression: "count()", Alias: "b"},
|
||||
{Expression: "count()", Alias: "c"},
|
||||
},
|
||||
wantExpression: "(__result_0 > 10 OR __result_1 > 20) AND __result_2 > 5",
|
||||
},
|
||||
// --- Happy path: comparison between two aggregation references ---
|
||||
{
|
||||
name: "comparison between aggregations",
|
||||
expression: "error_count > warn_count AND errors = warnings",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "sum(errors)", Alias: "error_count"},
|
||||
{Expression: "sum(warnings)", Alias: "warn_count"},
|
||||
{Expression: "sum(errors)", Alias: "errors"},
|
||||
{Expression: "sum(warnings)", Alias: "warnings"},
|
||||
},
|
||||
wantExpression: "__result_0 > __result_1 AND __result_2 = __result_3",
|
||||
},
|
||||
// --- Happy path: edge case (reserved keyword used as alias) ---
|
||||
{
|
||||
name: "reserved keyword as alias",
|
||||
expression: "sum > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "sum"},
|
||||
},
|
||||
wantExpression: "__result_0 > 100",
|
||||
},
|
||||
// --- Error: bare operand / missing comparison operator ---
|
||||
{
|
||||
name: "urinary expression alias",
|
||||
expression: "total_logs",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "bare expression (function call)",
|
||||
expression: "count()",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "bare operand without comparison",
|
||||
expression: "total_logs",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
// --- Error: parentheses mismatch (unclosed, unexpected, empty) ---
|
||||
{
|
||||
name: "unclosed parenthesis",
|
||||
expression: "(total_logs > 100 AND count() < 500",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "unexpected closing parenthesis",
|
||||
expression: "total_logs > 100)",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "dangling AND at end",
|
||||
expression: "total_logs > 100 AND",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "dangling OR at start",
|
||||
expression: "OR total_logs > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
// --- Error: dangling or malformed boolean operators (AND, OR) ---
|
||||
{
|
||||
name: "consecutive boolean operators",
|
||||
expression: "total_logs > 100 AND AND count() < 500",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
// --- Error: invalid operand types (string literals, boolean literal) ---
|
||||
{
|
||||
name: "string literal as comparison value",
|
||||
expression: "sum(bytes) = 'xyz'",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "sum(bytes)"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "HAVING expression cannot contain string literals",
|
||||
},
|
||||
{
|
||||
name: "string literal without quotes as comparison value",
|
||||
expression: "sum(bytes) = xyz",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "sum(bytes)"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "invalid references in HAVING expression: [xyz]",
|
||||
},
|
||||
{
|
||||
name: "double-quoted string literal",
|
||||
expression: `total > "threshold"`,
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "HAVING expression cannot contain string literals",
|
||||
},
|
||||
// --- Error: invalid or unknown references ---
|
||||
{
|
||||
name: "unknown identifier",
|
||||
expression: "unknown_alias > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "invalid references in HAVING expression: [unknown_alias]",
|
||||
},
|
||||
{
|
||||
name: "expression not in column map",
|
||||
expression: "sum(missing_field) > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "aggregation functions are not allowed in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "one valid one invalid reference",
|
||||
expression: "total > 100 AND ghost > 50",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "invalid references in HAVING expression: [ghost]",
|
||||
},
|
||||
{
|
||||
name: "__result ambiguous with multiple aggregations",
|
||||
expression: "__result > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
{Expression: "sum(bytes)"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "invalid references in HAVING expression: [__result]",
|
||||
},
|
||||
// --- Error: NOT syntax (must wrap comparison in parentheses) ---
|
||||
{
|
||||
name: "NOT without parentheses",
|
||||
expression: "NOT count() > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "empty expression",
|
||||
expression: "",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
// --- Error: malformed comparison (missing operand, operator, or connector) ---
|
||||
{
|
||||
name: "two comparisons without boolean connector",
|
||||
expression: "total > 100 count() < 500",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "out-of-range __result_N index",
|
||||
expression: "__result_9 > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "invalid references in HAVING expression: [__result_9]",
|
||||
},
|
||||
// --- Error: boolean literal as comparison value ---
|
||||
{
|
||||
name: "boolean literal as comparison value",
|
||||
expression: "count() > true",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "double NOT without valid grouping",
|
||||
expression: "NOT NOT (count() > 100)",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
// --- Error: invalid function calls (cascaded, wrong args) ---
|
||||
{
|
||||
name: "cascaded function calls",
|
||||
expression: "sum(count()) > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "aggregation functions are not allowed in HAVING expression",
|
||||
},
|
||||
// --- Error: empty or whitespace-only expression ---
|
||||
{
|
||||
name: "whitespace only expression",
|
||||
expression: " ",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
// --- Error: standalone parentheses ---
|
||||
{
|
||||
name: "only opening parenthesis",
|
||||
expression: "(",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "only closing parenthesis",
|
||||
expression: ")",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "empty parentheses",
|
||||
expression: "()",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
// --- Error: missing comparison operands or operator ---
|
||||
{
|
||||
name: "missing left operand",
|
||||
expression: "> 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "missing right operand",
|
||||
expression: "count() >",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "missing operator",
|
||||
expression: "count() 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "dangling OR at end",
|
||||
expression: "total > 100 OR",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "AND OR without second operand",
|
||||
expression: "total > 100 AND OR count() < 50",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
{
|
||||
name: "NOT without parentheses on alias",
|
||||
expression: "NOT total > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
// --- Error: invalid function call (not in column map) ---
|
||||
{
|
||||
name: "function call with multiple args not in column map",
|
||||
expression: "sum(a, b) > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "sum(a)"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "aggregation functions are not allowed in HAVING expression",
|
||||
},
|
||||
// --- Error: out-of-range __result index ---
|
||||
{
|
||||
name: "__result_1 out of range for single aggregation",
|
||||
expression: "__result_1 > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "invalid references in HAVING expression: [__result_1]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := NewHavingExpressionRewriter()
|
||||
traceAggs := toTraceAggregations(tt.aggregations)
|
||||
gotLogs, errLogs := r.RewriteForLogs(tt.expression, tt.aggregations)
|
||||
r2 := NewHavingExpressionRewriter()
|
||||
gotTraces, errTraces := r2.RewriteForTraces(tt.expression, traceAggs)
|
||||
if tt.wantErr {
|
||||
require.Error(t, errLogs)
|
||||
assert.ErrorContains(t, errLogs, tt.wantErrMsg)
|
||||
require.Error(t, errTraces)
|
||||
assert.ErrorContains(t, errTraces, tt.wantErrMsg)
|
||||
} else {
|
||||
require.NoError(t, errLogs)
|
||||
assert.Equal(t, tt.wantExpression, gotLogs)
|
||||
require.NoError(t, errTraces)
|
||||
assert.Equal(t, tt.wantExpression, gotTraces)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteForMetrics(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
aggregations []qbtypes.MetricAggregation
|
||||
wantExpression string
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
// --- Happy path: reference types (time/space aggregation, __result, bare metric) ---
|
||||
{
|
||||
name: "time aggregation reference",
|
||||
expression: "sum(cpu_usage) > 80",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationSum,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationUnspecified,
|
||||
},
|
||||
},
|
||||
wantExpression: "value > 80",
|
||||
},
|
||||
{
|
||||
name: "space aggregation reference",
|
||||
expression: "avg(cpu_usage) > 50",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
TimeAggregation: metrictypes.TimeAggregationUnspecified,
|
||||
},
|
||||
},
|
||||
wantExpression: "value > 50",
|
||||
},
|
||||
{
|
||||
name: "__result reference",
|
||||
expression: "__result > 90",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationSum,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationUnspecified,
|
||||
},
|
||||
},
|
||||
wantExpression: "value > 90",
|
||||
},
|
||||
{
|
||||
name: "bare metric name when no aggregations set",
|
||||
expression: "cpu_usage > 80",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationUnspecified,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationUnspecified,
|
||||
},
|
||||
},
|
||||
wantExpression: "value > 80",
|
||||
},
|
||||
{
|
||||
name: "combined space and time aggregation",
|
||||
expression: "avg(sum(cpu_usage)) > 50",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationSum,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
},
|
||||
},
|
||||
wantExpression: "value > 50",
|
||||
},
|
||||
// --- Happy path: comparison operators and arithmetic ---
|
||||
{
|
||||
name: "comparison operators and arithmetic",
|
||||
expression: "sum(cpu_usage) < 100 AND sum(cpu_usage) * 2 > 50",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationSum,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationUnspecified,
|
||||
},
|
||||
},
|
||||
wantExpression: "value < 100 AND value * 2 > 50",
|
||||
},
|
||||
// --- Error: invalid or unknown metric reference ---
|
||||
{
|
||||
name: "unknown metric reference",
|
||||
expression: "wrong_metric > 80",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationSum,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationUnspecified,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "invalid references in HAVING expression: [wrong_metric]",
|
||||
},
|
||||
// --- Error: empty or bare operand ---
|
||||
{
|
||||
name: "empty expression",
|
||||
expression: "",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationSum,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationUnspecified,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
// --- Error: string literal (not allowed in HAVING) ---
|
||||
{
|
||||
name: "string literal rejected",
|
||||
expression: "cpu_usage = 'high'",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationSum,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationUnspecified,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "HAVING expression cannot contain string literals",
|
||||
},
|
||||
// --- Error: bare operand (no comparison) ---
|
||||
{
|
||||
name: "bare operand without comparison",
|
||||
expression: "cpu_usage",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationSum,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationUnspecified,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "syntax error in HAVING expression",
|
||||
},
|
||||
// --- Error: aggregation not in column map ---
|
||||
{
|
||||
name: "aggregation not in column map",
|
||||
expression: "count(cpu_usage) > 10",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationSum,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationUnspecified,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "invalid references in HAVING expression",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := NewHavingExpressionRewriter()
|
||||
got, err := r.RewriteForMetrics(tt.expression, tt.aggregations)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, tt.wantErrMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantExpression, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar/filterquery"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar/filterquery"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar/filterquery"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar/filterquery"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
|
||||
@@ -412,7 +412,10 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
// Rewrite having expression to use SQL column names
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForLogs(query.Having.Expression, query.Aggregations)
|
||||
rewrittenExpr, err := rewriter.RewriteForLogs(query.Having.Expression, query.Aggregations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.Having(rewrittenExpr)
|
||||
}
|
||||
|
||||
@@ -438,7 +441,10 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForLogs(query.Having.Expression, query.Aggregations)
|
||||
rewrittenExpr, err := rewriter.RewriteForLogs(query.Having.Expression, query.Aggregations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.Having(rewrittenExpr)
|
||||
}
|
||||
|
||||
@@ -545,7 +551,10 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
|
||||
// Add having clause if needed
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForLogs(query.Having.Expression, query.Aggregations)
|
||||
rewrittenExpr, err := rewriter.RewriteForLogs(query.Having.Expression, query.Aggregations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.Having(rewrittenExpr)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHavingExpressionRewriter_LogQueries(t *testing.T) {
|
||||
@@ -14,6 +15,7 @@ func TestHavingExpressionRewriter_LogQueries(t *testing.T) {
|
||||
havingExpression string
|
||||
aggregations []qbtypes.LogAggregation
|
||||
expectedExpression string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single aggregation with alias",
|
||||
@@ -66,13 +68,34 @@ func TestHavingExpressionRewriter_LogQueries(t *testing.T) {
|
||||
},
|
||||
expectedExpression: "__result_1 > 10 AND __result_0 < 1000",
|
||||
},
|
||||
{
|
||||
name: "string literal in having expression",
|
||||
havingExpression: "count() > 'threshold'",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: ""},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unknown reference",
|
||||
havingExpression: "no_such_alias > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
result := rewriter.RewriteForLogs(tt.havingExpression, tt.aggregations)
|
||||
assert.Equal(t, tt.expectedExpression, result)
|
||||
result, err := rewriter.RewriteForLogs(tt.havingExpression, tt.aggregations)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedExpression, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,7 +647,10 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
|
||||
sb.GroupBy("ts")
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
|
||||
rewrittenExpr, err := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.Having(rewrittenExpr)
|
||||
}
|
||||
} else {
|
||||
@@ -655,7 +658,10 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
|
||||
sb.From("__spatial_aggregation_cte")
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
|
||||
rewrittenExpr, err := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.Where(rewrittenExpr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,7 +569,10 @@ func (b *traceQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForTraces(query.Having.Expression, query.Aggregations)
|
||||
rewrittenExpr, err := rewriter.RewriteForTraces(query.Having.Expression, query.Aggregations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.Having(rewrittenExpr)
|
||||
}
|
||||
|
||||
@@ -595,7 +598,10 @@ func (b *traceQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForTraces(query.Having.Expression, query.Aggregations)
|
||||
rewrittenExpr, err := rewriter.RewriteForTraces(query.Having.Expression, query.Aggregations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.Having(rewrittenExpr)
|
||||
}
|
||||
|
||||
@@ -701,7 +707,10 @@ func (b *traceQueryStatementBuilder) buildScalarQuery(
|
||||
// Add having clause if needed
|
||||
if query.Having != nil && query.Having.Expression != "" && !skipHaving {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForTraces(query.Having.Expression, query.Aggregations)
|
||||
rewrittenExpr, err := rewriter.RewriteForTraces(query.Having.Expression, query.Aggregations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.Having(rewrittenExpr)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package telemetrytraces
|
||||
import (
|
||||
"strings"
|
||||
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar/filterquery"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
|
||||
@@ -615,7 +615,10 @@ func (b *traceOperatorCTEBuilder) buildTimeSeriesQuery(ctx context.Context, sele
|
||||
combinedArgs := append(allGroupByArgs, allAggChArgs...)
|
||||
|
||||
// Add HAVING clause if specified
|
||||
b.addHavingClause(sb)
|
||||
err = b.addHavingClause(sb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...)
|
||||
return &qbtypes.Statement{
|
||||
@@ -727,7 +730,10 @@ func (b *traceOperatorCTEBuilder) buildTraceQuery(ctx context.Context, selectFro
|
||||
sb.GroupBy(groupByKeys...)
|
||||
}
|
||||
|
||||
b.addHavingClause(sb)
|
||||
err = b.addHavingClause(sb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderApplied := false
|
||||
for _, orderBy := range b.operator.Order {
|
||||
@@ -866,7 +872,10 @@ func (b *traceOperatorCTEBuilder) buildScalarQuery(ctx context.Context, selectFr
|
||||
combinedArgs := append(allGroupByArgs, allAggChArgs...)
|
||||
|
||||
// Add HAVING clause if specified
|
||||
b.addHavingClause(sb)
|
||||
err = b.addHavingClause(sb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...)
|
||||
return &qbtypes.Statement{
|
||||
@@ -875,12 +884,16 @@ func (b *traceOperatorCTEBuilder) buildScalarQuery(ctx context.Context, selectFr
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *traceOperatorCTEBuilder) addHavingClause(sb *sqlbuilder.SelectBuilder) {
|
||||
func (b *traceOperatorCTEBuilder) addHavingClause(sb *sqlbuilder.SelectBuilder) error {
|
||||
if b.operator.Having != nil && b.operator.Having.Expression != "" {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForTraces(b.operator.Having.Expression, b.operator.Aggregations)
|
||||
rewrittenExpr, err := rewriter.RewriteForTraces(b.operator.Having.Expression, b.operator.Aggregations)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sb.Having(rewrittenExpr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *traceOperatorCTEBuilder) addCTE(name, sql string, args []any, dependsOn []string) {
|
||||
|
||||
@@ -209,6 +209,12 @@ func (q *QueryBuilderQuery[T]) validateAggregations() error {
|
||||
}
|
||||
aliases[v.Alias] = true
|
||||
}
|
||||
if strings.Contains(strings.ToLower(v.Expression), " as ") {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"aliasing is not allowed in expression. Use `alias` field instead",
|
||||
)
|
||||
}
|
||||
case LogAggregation:
|
||||
if v.Expression == "" {
|
||||
aggId := fmt.Sprintf("aggregation #%d", i+1)
|
||||
@@ -231,6 +237,12 @@ func (q *QueryBuilderQuery[T]) validateAggregations() error {
|
||||
}
|
||||
aliases[v.Alias] = true
|
||||
}
|
||||
if strings.Contains(strings.ToLower(v.Expression), " as ") {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"aliasing is not allowed in expression. Use `alias` field instead",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/grammar/filterquery"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user