mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-16 18:02:09 +00:00
Compare commits
12 Commits
feat/cloud
...
tvats-add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d3c7956ac | ||
|
|
478958c129 | ||
|
|
7ab091f02d | ||
|
|
c173fe1cf9 | ||
|
|
1555153156 | ||
|
|
23a6801646 | ||
|
|
26a651667d | ||
|
|
9102e4ccc6 | ||
|
|
74b1df2941 | ||
|
|
a8348b6395 | ||
|
|
2aedf5f7e6 | ||
|
|
a77a4d4daa |
183
grammar/HavingExpression.g4
Normal file
183
grammar/HavingExpression.g4
Normal file
@@ -0,0 +1,183 @@
|
||||
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 + optional chaining with implicit AND if no OR is present
|
||||
andExpression
|
||||
: primary ( AND primary | primary )*
|
||||
;
|
||||
|
||||
// Primary: an optionally negated expression.
|
||||
// NOT can be applied to a parenthesized expression or a bare comparison.
|
||||
// E.g.: NOT (count() > 100 AND sum(bytes) < 500)
|
||||
// NOT count() > 100
|
||||
primary
|
||||
: NOT? LPAREN orExpression RPAREN
|
||||
| NOT? 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+)?
|
||||
;
|
||||
|
||||
// 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/filterquery/grammar"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
|
||||
70
pkg/parser/havingexpression/grammar/HavingExpression.interp
Normal file
70
pkg/parser/havingexpression/grammar/HavingExpression.interp
Normal file
@@ -0,0 +1,70 @@
|
||||
token literal names:
|
||||
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
|
||||
IDENTIFIER
|
||||
WS
|
||||
|
||||
rule names:
|
||||
query
|
||||
expression
|
||||
orExpression
|
||||
andExpression
|
||||
primary
|
||||
comparison
|
||||
compOp
|
||||
operand
|
||||
term
|
||||
factor
|
||||
atom
|
||||
functionCall
|
||||
functionArgs
|
||||
functionArg
|
||||
identifier
|
||||
|
||||
|
||||
atn:
|
||||
[4, 1, 22, 125, 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, 1, 3, 5, 3, 48, 8, 3, 10, 3, 12, 3, 51, 9, 3, 1, 4, 3, 4, 54, 8, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 61, 8, 4, 1, 4, 3, 4, 64, 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, 78, 8, 7, 10, 7, 12, 7, 81, 9, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 5, 8, 89, 8, 8, 10, 8, 12, 8, 92, 9, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 99, 8, 9, 1, 10, 1, 10, 1, 10, 3, 10, 104, 8, 10, 1, 11, 1, 11, 1, 11, 3, 11, 109, 8, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 5, 12, 116, 8, 12, 10, 12, 12, 12, 119, 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, 122, 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, 63, 1, 0, 0, 0, 10, 65, 1, 0, 0, 0, 12, 69, 1, 0, 0, 0, 14, 71, 1, 0, 0, 0, 16, 82, 1, 0, 0, 0, 18, 98, 1, 0, 0, 0, 20, 103, 1, 0, 0, 0, 22, 105, 1, 0, 0, 0, 24, 112, 1, 0, 0, 0, 26, 120, 1, 0, 0, 0, 28, 122, 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, 49, 3, 8, 4, 0, 44, 45, 5, 17, 0, 0, 45, 48, 3, 8, 4, 0, 46, 48, 3, 8, 4, 0, 47, 44, 1, 0, 0, 0, 47, 46, 1, 0, 0, 0, 48, 51, 1, 0, 0, 0, 49, 47, 1, 0, 0, 0, 49, 50, 1, 0, 0, 0, 50, 7, 1, 0, 0, 0, 51, 49, 1, 0, 0, 0, 52, 54, 5, 16, 0, 0, 53, 52, 1, 0, 0, 0, 53, 54, 1, 0, 0, 0, 54, 55, 1, 0, 0, 0, 55, 56, 5, 1, 0, 0, 56, 57, 3, 4, 2, 0, 57, 58, 5, 2, 0, 0, 58, 64, 1, 0, 0, 0, 59, 61, 5, 16, 0, 0, 60, 59, 1, 0, 0, 0, 60, 61, 1, 0, 0, 0, 61, 62, 1, 0, 0, 0, 62, 64, 3, 10, 5, 0, 63, 53, 1, 0, 0, 0, 63, 60, 1, 0, 0, 0, 64, 9, 1, 0, 0, 0, 65, 66, 3, 14, 7, 0, 66, 67, 3, 12, 6, 0, 67, 68, 3, 14, 7, 0, 68, 11, 1, 0, 0, 0, 69, 70, 7, 0, 0, 0, 70, 13, 1, 0, 0, 0, 71, 72, 6, 7, -1, 0, 72, 73, 3, 16, 8, 0, 73, 79, 1, 0, 0, 0, 74, 75, 10, 2, 0, 0, 75, 76, 7, 1, 0, 0, 76, 78, 3, 16, 8, 0, 77, 74, 1, 0, 0, 0, 78, 81, 1, 0, 0, 0, 79, 77, 1, 0, 0, 0, 79, 80, 1, 0, 0, 0, 80, 15, 1, 0, 0, 0, 81, 79, 1, 0, 0, 0, 82, 83, 6, 8, -1, 0, 83, 84, 3, 18, 9, 0, 84, 90, 1, 0, 0, 0, 85, 86, 10, 2, 0, 0, 86, 87, 7, 2, 0, 0, 87, 89, 3, 18, 9, 0, 88, 85, 1, 0, 0, 0, 89, 92, 1, 0, 0, 0, 90, 88, 1, 0, 0, 0, 90, 91, 1, 0, 0, 0, 91, 17, 1, 0, 0, 0, 92, 90, 1, 0, 0, 0, 93, 94, 5, 1, 0, 0, 94, 95, 3, 14, 7, 0, 95, 96, 5, 2, 0, 0, 96, 99, 1, 0, 0, 0, 97, 99, 3, 20, 10, 0, 98, 93, 1, 0, 0, 0, 98, 97, 1, 0, 0, 0, 99, 19, 1, 0, 0, 0, 100, 104, 3, 22, 11, 0, 101, 104, 3, 28, 14, 0, 102, 104, 5, 20, 0, 0, 103, 100, 1, 0, 0, 0, 103, 101, 1, 0, 0, 0, 103, 102, 1, 0, 0, 0, 104, 21, 1, 0, 0, 0, 105, 106, 5, 21, 0, 0, 106, 108, 5, 1, 0, 0, 107, 109, 3, 24, 12, 0, 108, 107, 1, 0, 0, 0, 108, 109, 1, 0, 0, 0, 109, 110, 1, 0, 0, 0, 110, 111, 5, 2, 0, 0, 111, 23, 1, 0, 0, 0, 112, 117, 3, 26, 13, 0, 113, 114, 5, 3, 0, 0, 114, 116, 3, 26, 13, 0, 115, 113, 1, 0, 0, 0, 116, 119, 1, 0, 0, 0, 117, 115, 1, 0, 0, 0, 117, 118, 1, 0, 0, 0, 118, 25, 1, 0, 0, 0, 119, 117, 1, 0, 0, 0, 120, 121, 5, 21, 0, 0, 121, 27, 1, 0, 0, 0, 122, 123, 5, 21, 0, 0, 123, 29, 1, 0, 0, 0, 12, 40, 47, 49, 53, 60, 63, 79, 90, 98, 103, 108, 117]
|
||||
36
pkg/parser/havingexpression/grammar/HavingExpression.tokens
Normal file
36
pkg/parser/havingexpression/grammar/HavingExpression.tokens
Normal file
@@ -0,0 +1,36 @@
|
||||
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
|
||||
IDENTIFIER=21
|
||||
WS=22
|
||||
'('=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,36 @@
|
||||
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
|
||||
IDENTIFIER=21
|
||||
WS=22
|
||||
'('=1
|
||||
')'=2
|
||||
','=3
|
||||
'!='=5
|
||||
'<>'=6
|
||||
'<'=7
|
||||
'<='=8
|
||||
'>'=9
|
||||
'>='=10
|
||||
'+'=11
|
||||
'-'=12
|
||||
'*'=13
|
||||
'/'=14
|
||||
'%'=15
|
||||
@@ -0,0 +1,112 @@
|
||||
// Code generated from grammar/HavingExpression.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // 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,69 @@
|
||||
// Code generated from grammar/HavingExpression.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // 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)
|
||||
}
|
||||
213
pkg/parser/havingexpression/grammar/havingexpression_lexer.go
Normal file
213
pkg/parser/havingexpression/grammar/havingexpression_lexer.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// Code generated from grammar/HavingExpression.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser
|
||||
|
||||
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", "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", "IDENTIFIER", "WS", "DIGIT",
|
||||
}
|
||||
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
|
||||
staticData.serializedATN = []int32{
|
||||
4, 0, 22, 189, 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, 1, 0, 1, 0, 1, 1, 1, 1, 1,
|
||||
2, 1, 2, 1, 3, 1, 3, 1, 3, 3, 3, 59, 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, 107, 8,
|
||||
18, 1, 19, 1, 19, 1, 20, 3, 20, 112, 8, 20, 1, 20, 4, 20, 115, 8, 20, 11,
|
||||
20, 12, 20, 116, 1, 20, 1, 20, 5, 20, 121, 8, 20, 10, 20, 12, 20, 124,
|
||||
9, 20, 3, 20, 126, 8, 20, 1, 20, 1, 20, 3, 20, 130, 8, 20, 1, 20, 4, 20,
|
||||
133, 8, 20, 11, 20, 12, 20, 134, 3, 20, 137, 8, 20, 1, 20, 3, 20, 140,
|
||||
8, 20, 1, 20, 1, 20, 4, 20, 144, 8, 20, 11, 20, 12, 20, 145, 1, 20, 1,
|
||||
20, 3, 20, 150, 8, 20, 1, 20, 4, 20, 153, 8, 20, 11, 20, 12, 20, 154, 3,
|
||||
20, 157, 8, 20, 3, 20, 159, 8, 20, 1, 21, 1, 21, 5, 21, 163, 8, 21, 10,
|
||||
21, 12, 21, 166, 9, 21, 1, 21, 1, 21, 1, 21, 5, 21, 171, 8, 21, 10, 21,
|
||||
12, 21, 174, 9, 21, 5, 21, 176, 8, 21, 10, 21, 12, 21, 179, 9, 21, 1, 22,
|
||||
4, 22, 182, 8, 22, 11, 22, 12, 22, 183, 1, 22, 1, 22, 1, 23, 1, 23, 0,
|
||||
0, 24, 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, 0, 1, 0, 16, 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, 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, 205, 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, 1, 49,
|
||||
1, 0, 0, 0, 3, 51, 1, 0, 0, 0, 5, 53, 1, 0, 0, 0, 7, 58, 1, 0, 0, 0, 9,
|
||||
60, 1, 0, 0, 0, 11, 63, 1, 0, 0, 0, 13, 66, 1, 0, 0, 0, 15, 68, 1, 0, 0,
|
||||
0, 17, 71, 1, 0, 0, 0, 19, 73, 1, 0, 0, 0, 21, 76, 1, 0, 0, 0, 23, 78,
|
||||
1, 0, 0, 0, 25, 80, 1, 0, 0, 0, 27, 82, 1, 0, 0, 0, 29, 84, 1, 0, 0, 0,
|
||||
31, 86, 1, 0, 0, 0, 33, 90, 1, 0, 0, 0, 35, 94, 1, 0, 0, 0, 37, 106, 1,
|
||||
0, 0, 0, 39, 108, 1, 0, 0, 0, 41, 158, 1, 0, 0, 0, 43, 160, 1, 0, 0, 0,
|
||||
45, 181, 1, 0, 0, 0, 47, 187, 1, 0, 0, 0, 49, 50, 5, 40, 0, 0, 50, 2, 1,
|
||||
0, 0, 0, 51, 52, 5, 41, 0, 0, 52, 4, 1, 0, 0, 0, 53, 54, 5, 44, 0, 0, 54,
|
||||
6, 1, 0, 0, 0, 55, 59, 5, 61, 0, 0, 56, 57, 5, 61, 0, 0, 57, 59, 5, 61,
|
||||
0, 0, 58, 55, 1, 0, 0, 0, 58, 56, 1, 0, 0, 0, 59, 8, 1, 0, 0, 0, 60, 61,
|
||||
5, 33, 0, 0, 61, 62, 5, 61, 0, 0, 62, 10, 1, 0, 0, 0, 63, 64, 5, 60, 0,
|
||||
0, 64, 65, 5, 62, 0, 0, 65, 12, 1, 0, 0, 0, 66, 67, 5, 60, 0, 0, 67, 14,
|
||||
1, 0, 0, 0, 68, 69, 5, 60, 0, 0, 69, 70, 5, 61, 0, 0, 70, 16, 1, 0, 0,
|
||||
0, 71, 72, 5, 62, 0, 0, 72, 18, 1, 0, 0, 0, 73, 74, 5, 62, 0, 0, 74, 75,
|
||||
5, 61, 0, 0, 75, 20, 1, 0, 0, 0, 76, 77, 5, 43, 0, 0, 77, 22, 1, 0, 0,
|
||||
0, 78, 79, 5, 45, 0, 0, 79, 24, 1, 0, 0, 0, 80, 81, 5, 42, 0, 0, 81, 26,
|
||||
1, 0, 0, 0, 82, 83, 5, 47, 0, 0, 83, 28, 1, 0, 0, 0, 84, 85, 5, 37, 0,
|
||||
0, 85, 30, 1, 0, 0, 0, 86, 87, 7, 0, 0, 0, 87, 88, 7, 1, 0, 0, 88, 89,
|
||||
7, 2, 0, 0, 89, 32, 1, 0, 0, 0, 90, 91, 7, 3, 0, 0, 91, 92, 7, 0, 0, 0,
|
||||
92, 93, 7, 4, 0, 0, 93, 34, 1, 0, 0, 0, 94, 95, 7, 1, 0, 0, 95, 96, 7,
|
||||
5, 0, 0, 96, 36, 1, 0, 0, 0, 97, 98, 7, 2, 0, 0, 98, 99, 7, 5, 0, 0, 99,
|
||||
100, 7, 6, 0, 0, 100, 107, 7, 7, 0, 0, 101, 102, 7, 8, 0, 0, 102, 103,
|
||||
7, 3, 0, 0, 103, 104, 7, 9, 0, 0, 104, 105, 7, 10, 0, 0, 105, 107, 7, 7,
|
||||
0, 0, 106, 97, 1, 0, 0, 0, 106, 101, 1, 0, 0, 0, 107, 38, 1, 0, 0, 0, 108,
|
||||
109, 7, 11, 0, 0, 109, 40, 1, 0, 0, 0, 110, 112, 3, 39, 19, 0, 111, 110,
|
||||
1, 0, 0, 0, 111, 112, 1, 0, 0, 0, 112, 114, 1, 0, 0, 0, 113, 115, 3, 47,
|
||||
23, 0, 114, 113, 1, 0, 0, 0, 115, 116, 1, 0, 0, 0, 116, 114, 1, 0, 0, 0,
|
||||
116, 117, 1, 0, 0, 0, 117, 125, 1, 0, 0, 0, 118, 122, 5, 46, 0, 0, 119,
|
||||
121, 3, 47, 23, 0, 120, 119, 1, 0, 0, 0, 121, 124, 1, 0, 0, 0, 122, 120,
|
||||
1, 0, 0, 0, 122, 123, 1, 0, 0, 0, 123, 126, 1, 0, 0, 0, 124, 122, 1, 0,
|
||||
0, 0, 125, 118, 1, 0, 0, 0, 125, 126, 1, 0, 0, 0, 126, 136, 1, 0, 0, 0,
|
||||
127, 129, 7, 7, 0, 0, 128, 130, 3, 39, 19, 0, 129, 128, 1, 0, 0, 0, 129,
|
||||
130, 1, 0, 0, 0, 130, 132, 1, 0, 0, 0, 131, 133, 3, 47, 23, 0, 132, 131,
|
||||
1, 0, 0, 0, 133, 134, 1, 0, 0, 0, 134, 132, 1, 0, 0, 0, 134, 135, 1, 0,
|
||||
0, 0, 135, 137, 1, 0, 0, 0, 136, 127, 1, 0, 0, 0, 136, 137, 1, 0, 0, 0,
|
||||
137, 159, 1, 0, 0, 0, 138, 140, 3, 39, 19, 0, 139, 138, 1, 0, 0, 0, 139,
|
||||
140, 1, 0, 0, 0, 140, 141, 1, 0, 0, 0, 141, 143, 5, 46, 0, 0, 142, 144,
|
||||
3, 47, 23, 0, 143, 142, 1, 0, 0, 0, 144, 145, 1, 0, 0, 0, 145, 143, 1,
|
||||
0, 0, 0, 145, 146, 1, 0, 0, 0, 146, 156, 1, 0, 0, 0, 147, 149, 7, 7, 0,
|
||||
0, 148, 150, 3, 39, 19, 0, 149, 148, 1, 0, 0, 0, 149, 150, 1, 0, 0, 0,
|
||||
150, 152, 1, 0, 0, 0, 151, 153, 3, 47, 23, 0, 152, 151, 1, 0, 0, 0, 153,
|
||||
154, 1, 0, 0, 0, 154, 152, 1, 0, 0, 0, 154, 155, 1, 0, 0, 0, 155, 157,
|
||||
1, 0, 0, 0, 156, 147, 1, 0, 0, 0, 156, 157, 1, 0, 0, 0, 157, 159, 1, 0,
|
||||
0, 0, 158, 111, 1, 0, 0, 0, 158, 139, 1, 0, 0, 0, 159, 42, 1, 0, 0, 0,
|
||||
160, 164, 7, 12, 0, 0, 161, 163, 7, 13, 0, 0, 162, 161, 1, 0, 0, 0, 163,
|
||||
166, 1, 0, 0, 0, 164, 162, 1, 0, 0, 0, 164, 165, 1, 0, 0, 0, 165, 177,
|
||||
1, 0, 0, 0, 166, 164, 1, 0, 0, 0, 167, 168, 5, 46, 0, 0, 168, 172, 7, 12,
|
||||
0, 0, 169, 171, 7, 13, 0, 0, 170, 169, 1, 0, 0, 0, 171, 174, 1, 0, 0, 0,
|
||||
172, 170, 1, 0, 0, 0, 172, 173, 1, 0, 0, 0, 173, 176, 1, 0, 0, 0, 174,
|
||||
172, 1, 0, 0, 0, 175, 167, 1, 0, 0, 0, 176, 179, 1, 0, 0, 0, 177, 175,
|
||||
1, 0, 0, 0, 177, 178, 1, 0, 0, 0, 178, 44, 1, 0, 0, 0, 179, 177, 1, 0,
|
||||
0, 0, 180, 182, 7, 14, 0, 0, 181, 180, 1, 0, 0, 0, 182, 183, 1, 0, 0, 0,
|
||||
183, 181, 1, 0, 0, 0, 183, 184, 1, 0, 0, 0, 184, 185, 1, 0, 0, 0, 185,
|
||||
186, 6, 22, 0, 0, 186, 46, 1, 0, 0, 0, 187, 188, 7, 15, 0, 0, 188, 48,
|
||||
1, 0, 0, 0, 20, 0, 58, 106, 111, 116, 122, 125, 129, 134, 136, 139, 145,
|
||||
149, 154, 156, 158, 164, 172, 177, 183, 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
|
||||
HavingExpressionLexerIDENTIFIER = 21
|
||||
HavingExpressionLexerWS = 22
|
||||
)
|
||||
100
pkg/parser/havingexpression/grammar/havingexpression_listener.go
Normal file
100
pkg/parser/havingexpression/grammar/havingexpression_listener.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Code generated from grammar/HavingExpression.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // 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)
|
||||
}
|
||||
2685
pkg/parser/havingexpression/grammar/havingexpression_parser.go
Normal file
2685
pkg/parser/havingexpression/grammar/havingexpression_parser.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
// Code generated from grammar/HavingExpression.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // 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{}
|
||||
}
|
||||
@@ -88,6 +88,8 @@ func (e *SyntaxErr) Error() string {
|
||||
exp := ""
|
||||
if len(e.Expected) > 0 {
|
||||
exp = "expecting one of {" + strings.Join(e.Expected, ", ") + "}" + " but got " + e.TokenTxt
|
||||
} else if e.Msg != "" {
|
||||
exp = e.Msg
|
||||
}
|
||||
return fmt.Sprintf("line %d:%d %s", e.Line, e.Col, exp)
|
||||
}
|
||||
|
||||
309
pkg/querybuilder/having_expression_validator.go
Normal file
309
pkg/querybuilder/having_expression_validator.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/havingexpression/grammar"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
// havingExpressionRewriteVisitor walks the parse tree of a HavingExpression in a single
|
||||
// pass, simultaneously rewriting user-facing references to their SQL column names and
|
||||
// collecting any references that could not be resolved.
|
||||
//
|
||||
// Each visit method reconstructs the expression string for its subtree:
|
||||
// - Structural nodes (orExpression, andExpression, comparison, arithmetic) are
|
||||
// reconstructed with canonical spacing.
|
||||
// - andExpression joins ALL primaries with " AND ", which naturally normalises any
|
||||
// implicit-AND adjacency (the old normalizeImplicitAND step).
|
||||
// - IdentifierContext looks the name up in columnMap; if found the SQL column name is
|
||||
// returned. If the name is already a valid SQL column (TO side of columnMap) it is
|
||||
// passed through unchanged. Otherwise it is added to invalid.
|
||||
// - FunctionCallContext looks the full call text (without whitespace, since WS is
|
||||
// skipped) up in columnMap; if found the SQL column name is returned, otherwise the
|
||||
// function name is added to invalid without recursing into its arguments.
|
||||
//
|
||||
// Complex columnMap keys whose argument contains nested function calls (e.g.
|
||||
// "avg(sum(cpu_usage))") cannot be parsed by the grammar, so they are substituted via
|
||||
// a targeted regex pre-pass in rewriteAndValidate before this visitor runs.
|
||||
type havingExpressionRewriteVisitor struct {
|
||||
columnMap map[string]string
|
||||
validColumns map[string]bool // TO-side values; identifiers already in SQL form pass through
|
||||
invalid []string
|
||||
seen map[string]bool
|
||||
}
|
||||
|
||||
func newHavingExpressionRewriteVisitor(columnMap map[string]string) *havingExpressionRewriteVisitor {
|
||||
validColumns := make(map[string]bool, len(columnMap))
|
||||
for _, col := range columnMap {
|
||||
validColumns[col] = true
|
||||
}
|
||||
return &havingExpressionRewriteVisitor{
|
||||
columnMap: columnMap,
|
||||
validColumns: validColumns,
|
||||
seen: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *havingExpressionRewriteVisitor) visitQuery(ctx grammar.IQueryContext) string {
|
||||
if ctx.Expression() == nil {
|
||||
return ""
|
||||
}
|
||||
return v.visitExpression(ctx.Expression())
|
||||
}
|
||||
|
||||
func (v *havingExpressionRewriteVisitor) visitExpression(ctx grammar.IExpressionContext) string {
|
||||
return v.visitOrExpression(ctx.OrExpression())
|
||||
}
|
||||
|
||||
func (v *havingExpressionRewriteVisitor) visitOrExpression(ctx grammar.IOrExpressionContext) string {
|
||||
andExprs := ctx.AllAndExpression()
|
||||
parts := make([]string, len(andExprs))
|
||||
for i, ae := range andExprs {
|
||||
parts[i] = v.visitAndExpression(ae)
|
||||
}
|
||||
return strings.Join(parts, " OR ")
|
||||
}
|
||||
|
||||
// visitAndExpression joins ALL primaries with " AND ".
|
||||
// The grammar rule `primary ( AND primary | primary )*` allows adjacent primaries
|
||||
// without an explicit AND (implicit AND). Joining all of them with " AND " here is
|
||||
// equivalent to the old normalizeImplicitAND step.
|
||||
func (v *havingExpressionRewriteVisitor) visitAndExpression(ctx grammar.IAndExpressionContext) string {
|
||||
primaries := ctx.AllPrimary()
|
||||
parts := make([]string, len(primaries))
|
||||
for i, p := range primaries {
|
||||
parts[i] = v.visitPrimary(p)
|
||||
}
|
||||
return strings.Join(parts, " AND ")
|
||||
}
|
||||
|
||||
func (v *havingExpressionRewriteVisitor) visitPrimary(ctx grammar.IPrimaryContext) string {
|
||||
if ctx.OrExpression() != nil {
|
||||
inner := v.visitOrExpression(ctx.OrExpression())
|
||||
if ctx.NOT() != nil {
|
||||
return "NOT (" + inner + ")"
|
||||
}
|
||||
return "(" + inner + ")"
|
||||
}
|
||||
if ctx.Comparison() == nil {
|
||||
return ""
|
||||
}
|
||||
inner := v.visitComparison(ctx.Comparison())
|
||||
if ctx.NOT() != nil {
|
||||
return "NOT (" + inner + ")"
|
||||
}
|
||||
return inner
|
||||
}
|
||||
|
||||
func (v *havingExpressionRewriteVisitor) visitComparison(ctx grammar.IComparisonContext) string {
|
||||
lhs := v.visitOperand(ctx.Operand(0))
|
||||
op := ctx.CompOp().GetText()
|
||||
rhs := v.visitOperand(ctx.Operand(1))
|
||||
return lhs + " " + op + " " + rhs
|
||||
}
|
||||
|
||||
func (v *havingExpressionRewriteVisitor) visitOperand(ctx grammar.IOperandContext) string {
|
||||
if ctx.Operand() != nil {
|
||||
left := v.visitOperand(ctx.Operand())
|
||||
right := v.visitTerm(ctx.Term())
|
||||
op := "+"
|
||||
if ctx.MINUS() != nil {
|
||||
op = "-"
|
||||
}
|
||||
return left + " " + op + " " + right
|
||||
}
|
||||
return v.visitTerm(ctx.Term())
|
||||
}
|
||||
|
||||
func (v *havingExpressionRewriteVisitor) visitTerm(ctx grammar.ITermContext) string {
|
||||
if ctx.Term() != nil {
|
||||
left := v.visitTerm(ctx.Term())
|
||||
right := v.visitFactor(ctx.Factor())
|
||||
op := "*"
|
||||
if ctx.SLASH() != nil {
|
||||
op = "/"
|
||||
} else if ctx.PERCENT() != nil {
|
||||
op = "%"
|
||||
}
|
||||
return left + " " + op + " " + right
|
||||
}
|
||||
return v.visitFactor(ctx.Factor())
|
||||
}
|
||||
|
||||
func (v *havingExpressionRewriteVisitor) visitFactor(ctx grammar.IFactorContext) string {
|
||||
if ctx.Operand() != nil {
|
||||
return "(" + v.visitOperand(ctx.Operand()) + ")"
|
||||
}
|
||||
if ctx.Atom() == nil {
|
||||
return ""
|
||||
}
|
||||
return v.visitAtom(ctx.Atom())
|
||||
}
|
||||
|
||||
func (v *havingExpressionRewriteVisitor) visitAtom(ctx grammar.IAtomContext) string {
|
||||
if ctx.FunctionCall() != nil {
|
||||
return v.visitFunctionCall(ctx.FunctionCall())
|
||||
}
|
||||
if ctx.Identifier() != nil {
|
||||
return v.visitIdentifier(ctx.Identifier())
|
||||
}
|
||||
// NUMBER token
|
||||
return ctx.NUMBER().GetText()
|
||||
}
|
||||
|
||||
// visitFunctionCall looks the full call text up in columnMap (WS is skipped so GetText
|
||||
// has no spaces, e.g. "sum(bytes)"). If found, returns the SQL column name. Otherwise
|
||||
// records the function name as invalid without recursing into the arguments.
|
||||
func (v *havingExpressionRewriteVisitor) visitFunctionCall(ctx grammar.IFunctionCallContext) string {
|
||||
fullText := ctx.GetText()
|
||||
if col, ok := v.columnMap[fullText]; ok {
|
||||
return col
|
||||
}
|
||||
funcName := ctx.IDENTIFIER().GetText()
|
||||
if !v.seen[funcName] {
|
||||
v.invalid = append(v.invalid, funcName)
|
||||
v.seen[funcName] = true
|
||||
}
|
||||
return fullText
|
||||
}
|
||||
|
||||
// visitIdentifier looks the identifier up in columnMap. If found, returns the SQL
|
||||
// column name. If the name is already a valid SQL column (validColumns), it is passed
|
||||
// through unchanged — this handles cases where the user writes the SQL column name
|
||||
// directly (e.g. __result_0). Otherwise records it as invalid.
|
||||
func (v *havingExpressionRewriteVisitor) visitIdentifier(ctx grammar.IIdentifierContext) string {
|
||||
name := ctx.IDENTIFIER().GetText()
|
||||
if col, ok := v.columnMap[name]; ok {
|
||||
return col
|
||||
}
|
||||
if v.validColumns[name] {
|
||||
return name
|
||||
}
|
||||
if !v.seen[name] {
|
||||
v.invalid = append(v.invalid, name)
|
||||
v.seen[name] = true
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// preSubstituteComplexKeys applies regex substitution for columnMap entries whose keys
|
||||
// cannot be parsed by the grammar. Specifically, a function-call key whose argument
|
||||
// part itself contains parentheses (e.g. "avg(sum(cpu_usage))") is a nested call that
|
||||
// the grammar's functionArg rule (IDENTIFIER only) cannot represent. These keys are
|
||||
// substituted before ANTLR parsing so the resulting expression is grammar-valid.
|
||||
//
|
||||
// Keys are applied longest-first to avoid partial replacements.
|
||||
func (r *HavingExpressionRewriter) preSubstituteComplexKeys(expression string) string {
|
||||
type kv struct{ k, v string }
|
||||
var pairs []kv
|
||||
for k, v := range r.columnMap {
|
||||
if isNestedFunctionCallKey(k) {
|
||||
pairs = append(pairs, kv{k, v})
|
||||
}
|
||||
}
|
||||
if len(pairs) == 0 {
|
||||
return expression
|
||||
}
|
||||
sort.Slice(pairs, func(i, j int) bool { return len(pairs[i].k) > len(pairs[j].k) })
|
||||
for _, p := range pairs {
|
||||
pat := regexp.MustCompile(`\b` + regexp.QuoteMeta(p.k))
|
||||
expression = pat.ReplaceAllString(expression, p.v)
|
||||
}
|
||||
return expression
|
||||
}
|
||||
|
||||
// isNestedFunctionCallKey returns true for keys that look like a function call whose
|
||||
// argument contains another function call (e.g. "avg(sum(cpu_usage))").
|
||||
func isNestedFunctionCallKey(key string) bool {
|
||||
idx := strings.IndexByte(key, '(')
|
||||
if idx < 0 {
|
||||
return false
|
||||
}
|
||||
return strings.ContainsRune(key[idx+1:], '(')
|
||||
}
|
||||
|
||||
// rewriteAndValidate is the single-pass implementation used by all RewriteFor* methods.
|
||||
//
|
||||
// Validation layers:
|
||||
// 1. Quoted string literals detected in the expression → descriptive error.
|
||||
// 2. The visitor runs on the parse tree, rewriting and collecting invalid references.
|
||||
// Unknown references (including unrecognised function calls) → lists valid references.
|
||||
// 3. ANTLR syntax errors → error with messages referencing the original token names.
|
||||
func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string, error) {
|
||||
// Layer 1 – reject quoted string literals.
|
||||
for _, ch := range expression {
|
||||
if ch == '\'' || ch == '"' {
|
||||
return "", errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"`Having` expression contains string literals",
|
||||
).WithAdditional("Aggregator results are numeric")
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-substitute any columnMap keys that the grammar cannot parse (nested calls).
|
||||
expression = r.preSubstituteComplexKeys(expression)
|
||||
|
||||
// Parse the expression once.
|
||||
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)
|
||||
|
||||
tree := p.Query()
|
||||
|
||||
// Layer 2 – run the combined visitor and report any unresolved references.
|
||||
// This runs before the syntax error check so that expressions with recoverable
|
||||
// parse errors (e.g. sum(count())) still produce an actionable "invalid reference"
|
||||
// message rather than a raw syntax error.
|
||||
v := newHavingExpressionRewriteVisitor(r.columnMap)
|
||||
result := v.visitQuery(tree)
|
||||
|
||||
if len(v.invalid) > 0 {
|
||||
sort.Strings(v.invalid)
|
||||
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]",
|
||||
strings.Join(v.invalid, ", "),
|
||||
).WithAdditional("Valid references are: [" + strings.Join(validKeys, ", ") + "]")
|
||||
}
|
||||
|
||||
// Layer 3 – ANTLR syntax errors. We parse the original expression, so error messages
|
||||
// already reference the user's own token names; no re-parsing is needed.
|
||||
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",
|
||||
).WithAdditional(detail)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package querybuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
@@ -19,19 +18,28 @@ func NewHavingExpressionRewriter() *HavingExpressionRewriter {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *HavingExpressionRewriter) RewriteForTraces(expression string, aggregations []qbtypes.TraceAggregation) string {
|
||||
func (r *HavingExpressionRewriter) RewriteForTraces(expression string, aggregations []qbtypes.TraceAggregation) (string, error) {
|
||||
if len(strings.TrimSpace(expression)) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
r.buildTraceColumnMap(aggregations)
|
||||
return r.rewriteExpression(expression)
|
||||
return r.rewriteAndValidate(expression)
|
||||
}
|
||||
|
||||
func (r *HavingExpressionRewriter) RewriteForLogs(expression string, aggregations []qbtypes.LogAggregation) string {
|
||||
func (r *HavingExpressionRewriter) RewriteForLogs(expression string, aggregations []qbtypes.LogAggregation) (string, error) {
|
||||
if len(strings.TrimSpace(expression)) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
r.buildLogColumnMap(aggregations)
|
||||
return r.rewriteExpression(expression)
|
||||
return r.rewriteAndValidate(expression)
|
||||
}
|
||||
|
||||
func (r *HavingExpressionRewriter) RewriteForMetrics(expression string, aggregations []qbtypes.MetricAggregation) string {
|
||||
func (r *HavingExpressionRewriter) RewriteForMetrics(expression string, aggregations []qbtypes.MetricAggregation) (string, error) {
|
||||
if len(strings.TrimSpace(expression)) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
r.buildMetricColumnMap(aggregations)
|
||||
return r.rewriteExpression(expression)
|
||||
return r.rewriteAndValidate(expression)
|
||||
}
|
||||
|
||||
func (r *HavingExpressionRewriter) buildTraceColumnMap(aggregations []qbtypes.TraceAggregation) {
|
||||
@@ -102,52 +110,3 @@ func (r *HavingExpressionRewriter) buildMetricColumnMap(aggregations []qbtypes.M
|
||||
r.columnMap[fmt.Sprintf("__result%d", idx)] = sqlColumn
|
||||
}
|
||||
}
|
||||
|
||||
func (r *HavingExpressionRewriter) rewriteExpression(expression string) string {
|
||||
quotedStrings := make(map[string]string)
|
||||
quotePattern := regexp.MustCompile(`'[^']*'|"[^"]*"`)
|
||||
quotedIdx := 0
|
||||
|
||||
expression = quotePattern.ReplaceAllStringFunc(expression, func(match string) string {
|
||||
placeholder := fmt.Sprintf("__QUOTED_%d__", quotedIdx)
|
||||
quotedStrings[placeholder] = match
|
||||
quotedIdx++
|
||||
return placeholder
|
||||
})
|
||||
|
||||
type mapping struct {
|
||||
from string
|
||||
to string
|
||||
}
|
||||
|
||||
mappings := make([]mapping, 0, len(r.columnMap))
|
||||
for from, to := range r.columnMap {
|
||||
mappings = append(mappings, mapping{from: from, to: to})
|
||||
}
|
||||
|
||||
for i := 0; i < len(mappings); i++ {
|
||||
for j := i + 1; j < len(mappings); j++ {
|
||||
if len(mappings[j].from) > len(mappings[i].from) {
|
||||
mappings[i], mappings[j] = mappings[j], mappings[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range mappings {
|
||||
if strings.Contains(m.from, "(") {
|
||||
// escape special regex characters in the function name
|
||||
escapedFrom := regexp.QuoteMeta(m.from)
|
||||
pattern := regexp.MustCompile(`\b` + escapedFrom)
|
||||
expression = pattern.ReplaceAllString(expression, m.to)
|
||||
} else {
|
||||
pattern := regexp.MustCompile(`\b` + regexp.QuoteMeta(m.from) + `\b`)
|
||||
expression = pattern.ReplaceAllString(expression, m.to)
|
||||
}
|
||||
}
|
||||
|
||||
for placeholder, original := range quotedStrings {
|
||||
expression = strings.Replace(expression, placeholder, original, 1)
|
||||
}
|
||||
|
||||
return expression
|
||||
}
|
||||
|
||||
733
pkg/querybuilder/having_rewriter_test.go
Normal file
733
pkg/querybuilder/having_rewriter_test.go
Normal file
@@ -0,0 +1,733 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"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
|
||||
wantAdditional []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, implicit AND) ---
|
||||
{
|
||||
name: "two comparisons without boolean connector",
|
||||
expression: "total > 100 count() < 500",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantExpression: "__result_0 > 100 AND __result_0 < 500",
|
||||
},
|
||||
{
|
||||
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: "NOT without parentheses",
|
||||
expression: "NOT count() > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantExpression: "NOT (__result_0 > 100)",
|
||||
},
|
||||
{
|
||||
name: "NOT without parentheses on alias",
|
||||
expression: "NOT total > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantExpression: "NOT (__result_0 > 100)",
|
||||
},
|
||||
// --- Error: NOT syntax ---
|
||||
{
|
||||
name: "double NOT without valid grouping",
|
||||
expression: "NOT NOT (count() > 100)",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Syntax error in `Having` expression",
|
||||
wantAdditional: []string{"line 1:4 expecting one of {(, ), AND, IDENTIFIER, NOT, number} but got 'NOT'"},
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
// --- Happy path: empty or whitespace-only expression ---
|
||||
{
|
||||
name: "empty expression",
|
||||
expression: "",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantExpression: "",
|
||||
},
|
||||
{
|
||||
name: "whitespace only expression",
|
||||
expression: " ",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantExpression: "",
|
||||
},
|
||||
// --- Error: bare operand / missing comparison operator ---
|
||||
{
|
||||
name: "bare expression (function call)",
|
||||
expression: "count()",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Syntax error in `Having` expression",
|
||||
wantAdditional: []string{"line 1:7 expecting one of {!=, '+', <, <=, <>, =, >, >=} but got EOF"},
|
||||
},
|
||||
{
|
||||
name: "bare operand without comparison",
|
||||
expression: "total_logs",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Syntax error in `Having` expression",
|
||||
wantAdditional: []string{"line 1:10 expecting one of {!=, '+', <, <=, <>, =, >, >=} but got EOF"},
|
||||
},
|
||||
// --- 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",
|
||||
wantAdditional: []string{"line 1:35 expecting one of {), ,} but got EOF"},
|
||||
},
|
||||
{
|
||||
name: "unexpected closing parenthesis",
|
||||
expression: "total_logs > 100)",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total_logs"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Syntax error in `Having` expression",
|
||||
wantAdditional: []string{"line 1:16 extraneous input ')' expecting <EOF>"},
|
||||
},
|
||||
{
|
||||
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",
|
||||
wantAdditional: []string{"line 1:20 expecting one of {(, ), AND, IDENTIFIER, NOT, number} but got EOF"},
|
||||
},
|
||||
{
|
||||
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",
|
||||
wantAdditional: []string{"line 1:0 expecting one of {(, ), AND, IDENTIFIER, NOT, number} but got 'OR'"},
|
||||
},
|
||||
// --- 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",
|
||||
wantAdditional: []string{"line 1:21 expecting one of {(, ), AND, IDENTIFIER, NOT, number} but got 'AND'"},
|
||||
},
|
||||
// --- 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 contains string literals",
|
||||
wantAdditional: []string{"Aggregator results are numeric"},
|
||||
},
|
||||
{
|
||||
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]",
|
||||
wantAdditional: []string{"Valid references are: [__result, __result0, sum(bytes)]"},
|
||||
},
|
||||
{
|
||||
name: "double-quoted string literal",
|
||||
expression: `total > "threshold"`,
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "`Having` expression contains string literals",
|
||||
wantAdditional: []string{"Aggregator results are numeric"},
|
||||
},
|
||||
// --- 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]",
|
||||
wantAdditional: []string{"Valid references are: [__result, __result0, count(), total]"},
|
||||
},
|
||||
{
|
||||
name: "expression not in column map",
|
||||
expression: "sum(missing_field) > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Invalid references in `Having` expression: [sum]",
|
||||
wantAdditional: []string{"Valid references are: [__result, __result0, count()]"},
|
||||
},
|
||||
{
|
||||
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]",
|
||||
wantAdditional: []string{"Valid references are: [__result, __result0, count(), total]"},
|
||||
},
|
||||
{
|
||||
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]",
|
||||
wantAdditional: []string{"Valid references are: [__result0, __result1, count(), sum(bytes)]"},
|
||||
},
|
||||
{
|
||||
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]",
|
||||
wantAdditional: []string{"Valid references are: [__result, __result0, count()]"},
|
||||
},
|
||||
// --- 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",
|
||||
wantAdditional: []string{"line 1:10 expecting one of {(, ), IDENTIFIER, number} but got 'true'"},
|
||||
},
|
||||
// --- Error: invalid function calls (cascaded, wrong args) ---
|
||||
{
|
||||
name: "cascaded function calls",
|
||||
expression: "sum(count()) > 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Invalid references in `Having` expression: [sum]",
|
||||
wantAdditional: []string{"Valid references are: [__result, __result0, count()]"},
|
||||
},
|
||||
// --- Error: standalone parentheses ---
|
||||
{
|
||||
name: "only opening parenthesis",
|
||||
expression: "(",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Syntax error in `Having` expression",
|
||||
wantAdditional: []string{"line 1:1 expecting one of {(, ), AND, IDENTIFIER, NOT, number} but got EOF"},
|
||||
},
|
||||
{
|
||||
name: "only closing parenthesis",
|
||||
expression: ")",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Syntax error in `Having` expression",
|
||||
wantAdditional: []string{"line 1:0 expecting one of {(, ), AND, IDENTIFIER, NOT, number} but got ')'"},
|
||||
},
|
||||
{
|
||||
name: "empty parentheses",
|
||||
expression: "()",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Syntax error in `Having` expression",
|
||||
wantAdditional: []string{"line 1:1 expecting one of {(, ), AND, IDENTIFIER, NOT, number} but got ')'"},
|
||||
},
|
||||
// --- 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",
|
||||
wantAdditional: []string{"line 1:0 expecting one of {(, ), AND, IDENTIFIER, NOT, number} but got '>'; line 1:5 expecting one of {!=, '+', <, <=, <>, =, >, >=} but got EOF"},
|
||||
},
|
||||
{
|
||||
name: "missing right operand",
|
||||
expression: "count() >",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Syntax error in `Having` expression",
|
||||
wantAdditional: []string{"line 1:9 expecting one of {(, ), IDENTIFIER, number} but got EOF"},
|
||||
},
|
||||
{
|
||||
name: "missing operator",
|
||||
expression: "count() 100",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Syntax error in `Having` expression",
|
||||
wantAdditional: []string{"line 1:8 expecting one of {!=, '+', <, <=, <>, =, >, >=} but got '100'"},
|
||||
},
|
||||
{
|
||||
name: "dangling OR at end",
|
||||
expression: "total > 100 OR",
|
||||
aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()", Alias: "total"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "Syntax error in `Having` expression",
|
||||
wantAdditional: []string{"line 1:14 expecting one of {(, ), AND, IDENTIFIER, NOT, number} but got EOF"},
|
||||
},
|
||||
{
|
||||
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",
|
||||
wantAdditional: []string{"line 1:16 expecting one of {(, ), AND, IDENTIFIER, NOT, number} but got 'OR'"},
|
||||
},
|
||||
// --- 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: "Invalid references in `Having` expression: [sum]",
|
||||
wantAdditional: []string{"Valid references are: [__result, __result0, sum(a)]"},
|
||||
},
|
||||
// --- 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]",
|
||||
wantAdditional: []string{"Valid references are: [__result, __result0, count()]"},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
_, _, _, _, _, additionalLogs := errors.Unwrapb(errLogs)
|
||||
assert.Equal(t, tt.wantAdditional, additionalLogs)
|
||||
require.Error(t, errTraces)
|
||||
assert.ErrorContains(t, errTraces, tt.wantErrMsg)
|
||||
_, _, _, _, _, additionalTraces := errors.Unwrapb(errTraces)
|
||||
assert.Equal(t, tt.wantAdditional, additionalTraces)
|
||||
} 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
|
||||
wantAdditional []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",
|
||||
},
|
||||
// --- Happy path: empty or bare operand ---
|
||||
{
|
||||
name: "empty expression",
|
||||
expression: "",
|
||||
aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationSum,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationUnspecified,
|
||||
},
|
||||
},
|
||||
wantExpression: "",
|
||||
},
|
||||
// --- 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]",
|
||||
wantAdditional: []string{"Valid references are: [__result, __result0, sum(cpu_usage)]"},
|
||||
},
|
||||
// --- 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 contains string literals",
|
||||
wantAdditional: []string{"Aggregator results are numeric"},
|
||||
},
|
||||
// --- 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: "Invalid references in `Having` expression: [cpu_usage]",
|
||||
wantAdditional: []string{"Valid references are: [__result, __result0, sum(cpu_usage)]"},
|
||||
},
|
||||
// --- 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: [count]",
|
||||
wantAdditional: []string{"Valid references are: [__result, __result0, sum(cpu_usage)]"},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
_, _, _, _, _, additional := errors.Unwrapb(err)
|
||||
assert.Equal(t, tt.wantAdditional, additional)
|
||||
} 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/filterquery/grammar"
|
||||
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/filterquery/grammar"
|
||||
"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/filterquery/grammar"
|
||||
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/filterquery/grammar"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
@@ -467,7 +467,7 @@ func TestVisitKey(t *testing.T) {
|
||||
expectedWarnings: nil,
|
||||
expectedMainWrnURL: "",
|
||||
},
|
||||
{
|
||||
{
|
||||
name: "only attribute.custom_field is selected",
|
||||
keyText: "attribute.attribute.custom_field",
|
||||
fieldKeys: map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,7 +581,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 if metricType == metrictypes.HistogramType && spaceAgg == metrictypes.SpaceAggregationCount && query.Aggregations[0].ComparisonSpaceAggregationParam != nil {
|
||||
@@ -613,7 +616,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/filterquery/grammar"
|
||||
"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) {
|
||||
|
||||
@@ -237,6 +237,12 @@ func (q *QueryBuilderQuery[T]) validateAggregations(requestType RequestType) err
|
||||
}
|
||||
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)
|
||||
@@ -259,6 +265,12 @@ func (q *QueryBuilderQuery[T]) validateAggregations(requestType RequestType) err
|
||||
}
|
||||
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/filterquery/grammar"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ echo "Generating Go parser..."
|
||||
mkdir -p pkg/parser
|
||||
|
||||
# Generate Go parser
|
||||
antlr -visitor -Dlanguage=Go -o pkg/parser grammar/FilterQuery.g4
|
||||
antlr -visitor -Dlanguage=Go -o pkg/parser/filterquery grammar/FilterQuery.g4
|
||||
antlr -visitor -Dlanguage=Go -o pkg/parser/havingexpression grammar/HavingExpression.g4
|
||||
|
||||
echo "Go parser generation complete"
|
||||
|
||||
Reference in New Issue
Block a user