fix: added validations for having expression (#10286)

* fix: added validations for having expression

* fix: added extra validation and unit tests

* fix: added antlr based parsing for validation

* fix: added more unit tests

* fix: removed validation on having in range request validations

* fix: generated lexer files and added more unit tests

* fix: edge cases

* fix: added cmnd to scripts for generating lexer

* fix: use std libg sorting instead of selection sort

* fix: support implicit and

* fix: allow bare not in expression

* fix: added suggestion for having expression

* fix: typo

* fix: added more unit tests, handle white space difference in aggregation exp and having exp

* fix: added support for in and not, updated errors

* fix: added support for brackets list

* fix: lint error

* fix: handle non spaced expression

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
Tushar Vats
2026-04-02 03:52:11 +00:00
committed by GitHub
co-authored by Srikanth Chekuri
parent 5abfd0732a
commit f71d5bf8f1
40 changed files with 6696 additions and 87 deletions
+256
View File
@@ -0,0 +1,256 @@
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 / IN-test.
// E.g.: NOT (count() > 100 AND sum(bytes) < 500)
// NOT count() > 100
// count() IN (1, 2, 3) -- NOT here is part of comparison, see below
// count() NOT IN (1, 2, 3)
primary
: NOT? LPAREN orExpression RPAREN
| NOT? comparison
;
/*
* Comparison between two arithmetic operands, or an IN / NOT IN membership test.
* E.g.: count() > 100, total_duration >= 500, __result_0 != 0
* count() IN (1, 2, 3), sum(bytes) NOT IN (0, -1)
* count() IN [1, 2, 3], sum(bytes) NOT IN [0, -1]
*/
comparison
: operand compOp operand
| operand NOT? IN LPAREN inList RPAREN
| operand NOT? IN LBRACK inList RBRACK
;
compOp
: EQUALS
| NOT_EQUALS
| NEQ
| LT
| LE
| GT
| GE
;
/*
* IN-list: a comma-separated list of numeric literals, each optionally signed.
* E.g.: (1, 2, 3), [100, 200, 500], (-1, 0, 1)
*/
inList
: signedNumber ( COMMA signedNumber )*
;
/*
* A signed number allows an optional leading +/- before a numeric literal.
* Used in IN-lists where a bare minus is unambiguous (no binary operand to the left).
*/
signedNumber
: (PLUS | MINUS)? NUMBER
;
/*
* 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, parenthesized operands, or unary-signed sub-factors.
* E.g.: (sum(a) + sum(b)) * 2 > 100, -count() > 0, -(avg(x) + 1) > 0
* -10 (unary minus applied to the literal 10), count() - 10 > 0
*
* Note: the NUMBER rule does NOT include a leading sign, so `-10` is always
* tokenised as MINUS followed by NUMBER(10). Unary minus in `factor` handles
* negative literals just as it handles negative function calls or identifiers,
* and the binary MINUS in `operand` handles `count()-10` naturally.
*/
factor
: (PLUS | MINUS) 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
* - string literals: 'xyz' — recognized so we can give a friendly error
*
* String literals in HAVING are always invalid (aggregator results are numeric),
* but we accept them here so the visitor can produce a clear error message instead
* of a raw syntax error.
*/
atom
: functionCall
| identifier
| NUMBER
| STRING
;
/*
* Aggregate function calls, e.g.:
* count(), sum(bytes), avg(duration_nano)
* countIf(level='error'), sumIf(bytes, status > 400)
* p99(duration), avg(sum(cpu_usage))
*
* Function arguments are parsed as a permissive token sequence (funcArgToken+)
* so that complex aggregation expressions — including nested function calls and
* filter predicates with string literals — can be referenced verbatim in the
* HAVING expression. The visitor looks up the full call text (whitespace-free,
* via ctx.GetText()) in the column map, which stores normalized (space-stripped)
* aggregation expression keys.
*/
functionCall
: IDENTIFIER LPAREN functionArgList? RPAREN
;
functionArgList
: funcArg ( COMMA funcArg )*
;
/*
* A single function argument is one or more consecutive arg-tokens.
* Commas at the top level separate arguments; closing parens terminate the list.
*/
funcArg
: funcArgToken+
;
/*
* Permissive token set for function argument content. Covers:
* - simple identifiers: bytes, duration
* - string literals: 'error', "info"
* - numeric literals: 200, 3.14
* - comparison operators: level='error', status > 400
* - arithmetic operators: x + y
* - boolean connectives: level='error' AND status=200
* - balanced parens: nested calls like sum(duration)
*/
funcArgToken
: IDENTIFIER
| STRING
| NUMBER
| BOOL
| EQUALS | NOT_EQUALS | NEQ | LT | LE | GT | GE
| PLUS | MINUS | STAR | SLASH | PERCENT
| NOT | AND | OR
| LPAREN funcArgToken* RPAREN
;
// Identifier references: aliases, field names, result references
// Examples: total_logs, error_count, __result, __result_0, __result0, p99
identifier
: IDENTIFIER
;
/*
* Lexer Rules
*/
// Punctuation
LPAREN : '(' ;
RPAREN : ')' ;
LBRACK : '[' ;
RBRACK : ']' ;
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] ;
IN : [Ii][Nn] ;
// Boolean constants (case-insensitive)
BOOL
: [Tt][Rr][Uu][Ee]
| [Ff][Aa][Ll][Ss][Ee]
;
fragment SIGN : [+-] ;
// Numbers: digits, optional decimal, optional scientific notation.
// No leading sign — a leading +/- is always a separate PLUS/MINUS token, which
// lets the parser treat it as either a binary operator (count()-10) or unary
// sign (-count(), -10). Signed exponents like 1e-3 remain valid.
// E.g.: 100, 0.5, 1.5e3, .75
NUMBER
: DIGIT+ ('.' DIGIT*)? ([eE] SIGN? DIGIT+)?
| '.' 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_]* )*
;
// Quoted string literals (single or double-quoted).
// These are valid tokens inside function arguments (e.g. countIf(level='error'))
// but are always rejected in comparison-operand position by the visitor.
STRING
: '\'' (~'\'')* '\''
| '"' (~'"')* '"'
;
// Skip whitespace
WS
: [ \t\r\n]+ -> skip
;
fragment DIGIT : [0-9] ;
+1 -1
View File
@@ -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"
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
LPAREN=1
RPAREN=2
LBRACK=3
RBRACK=4
COMMA=5
EQUALS=6
NOT_EQUALS=7
NEQ=8
LT=9
LE=10
GT=11
GE=12
PLUS=13
MINUS=14
STAR=15
SLASH=16
PERCENT=17
NOT=18
AND=19
OR=20
IN=21
BOOL=22
NUMBER=23
IDENTIFIER=24
STRING=25
WS=26
'('=1
')'=2
'['=3
']'=4
','=5
'!='=7
'<>'=8
'<'=9
'<='=10
'>'=11
'>='=12
'+'=13
'-'=14
'*'=15
'/'=16
'%'=17
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
LPAREN=1
RPAREN=2
LBRACK=3
RBRACK=4
COMMA=5
EQUALS=6
NOT_EQUALS=7
NEQ=8
LT=9
LE=10
GT=11
GE=12
PLUS=13
MINUS=14
STAR=15
SLASH=16
PERCENT=17
NOT=18
AND=19
OR=20
IN=21
BOOL=22
NUMBER=23
IDENTIFIER=24
STRING=25
WS=26
'('=1
')'=2
'['=3
']'=4
','=5
'!='=7
'<>'=8
'<'=9
'<='=10
'>'=11
'>='=12
'+'=13
'-'=14
'*'=15
'/'=16
'%'=17
@@ -0,0 +1,130 @@
// 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) {}
// EnterInList is called when production inList is entered.
func (s *BaseHavingExpressionListener) EnterInList(ctx *InListContext) {}
// ExitInList is called when production inList is exited.
func (s *BaseHavingExpressionListener) ExitInList(ctx *InListContext) {}
// EnterSignedNumber is called when production signedNumber is entered.
func (s *BaseHavingExpressionListener) EnterSignedNumber(ctx *SignedNumberContext) {}
// ExitSignedNumber is called when production signedNumber is exited.
func (s *BaseHavingExpressionListener) ExitSignedNumber(ctx *SignedNumberContext) {}
// 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) {}
// EnterFunctionArgList is called when production functionArgList is entered.
func (s *BaseHavingExpressionListener) EnterFunctionArgList(ctx *FunctionArgListContext) {}
// ExitFunctionArgList is called when production functionArgList is exited.
func (s *BaseHavingExpressionListener) ExitFunctionArgList(ctx *FunctionArgListContext) {}
// EnterFuncArg is called when production funcArg is entered.
func (s *BaseHavingExpressionListener) EnterFuncArg(ctx *FuncArgContext) {}
// ExitFuncArg is called when production funcArg is exited.
func (s *BaseHavingExpressionListener) ExitFuncArg(ctx *FuncArgContext) {}
// EnterFuncArgToken is called when production funcArgToken is entered.
func (s *BaseHavingExpressionListener) EnterFuncArgToken(ctx *FuncArgTokenContext) {}
// ExitFuncArgToken is called when production funcArgToken is exited.
func (s *BaseHavingExpressionListener) ExitFuncArgToken(ctx *FuncArgTokenContext) {}
// 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,81 @@
// 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) VisitInList(ctx *InListContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseHavingExpressionVisitor) VisitSignedNumber(ctx *SignedNumberContext) 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) VisitFunctionArgList(ctx *FunctionArgListContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseHavingExpressionVisitor) VisitFuncArg(ctx *FuncArgContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseHavingExpressionVisitor) VisitFuncArgToken(ctx *FuncArgTokenContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseHavingExpressionVisitor) VisitIdentifier(ctx *IdentifierContext) interface{} {
return v.VisitChildren(ctx)
}
@@ -0,0 +1,232 @@
// 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", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "PLUS", "MINUS", "STAR", "SLASH", "PERCENT",
"NOT", "AND", "OR", "IN", "BOOL", "NUMBER", "IDENTIFIER", "STRING",
"WS",
}
staticData.RuleNames = []string{
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "PLUS", "MINUS", "STAR", "SLASH", "PERCENT",
"NOT", "AND", "OR", "IN", "BOOL", "SIGN", "NUMBER", "IDENTIFIER", "STRING",
"WS", "DIGIT",
}
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
staticData.serializedATN = []int32{
4, 0, 26, 216, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2,
10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15,
7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7,
20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25,
2, 26, 7, 26, 2, 27, 7, 27, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1,
3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 3, 5, 71, 8, 5, 1, 6, 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,
11, 1, 12, 1, 12, 1, 13, 1, 13, 1, 14, 1, 14, 1, 15, 1, 15, 1, 16, 1, 16,
1, 17, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1,
19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21,
1, 21, 1, 21, 3, 21, 122, 8, 21, 1, 22, 1, 22, 1, 23, 4, 23, 127, 8, 23,
11, 23, 12, 23, 128, 1, 23, 1, 23, 5, 23, 133, 8, 23, 10, 23, 12, 23, 136,
9, 23, 3, 23, 138, 8, 23, 1, 23, 1, 23, 3, 23, 142, 8, 23, 1, 23, 4, 23,
145, 8, 23, 11, 23, 12, 23, 146, 3, 23, 149, 8, 23, 1, 23, 1, 23, 4, 23,
153, 8, 23, 11, 23, 12, 23, 154, 1, 23, 1, 23, 3, 23, 159, 8, 23, 1, 23,
4, 23, 162, 8, 23, 11, 23, 12, 23, 163, 3, 23, 166, 8, 23, 3, 23, 168,
8, 23, 1, 24, 1, 24, 5, 24, 172, 8, 24, 10, 24, 12, 24, 175, 9, 24, 1,
24, 1, 24, 1, 24, 5, 24, 180, 8, 24, 10, 24, 12, 24, 183, 9, 24, 5, 24,
185, 8, 24, 10, 24, 12, 24, 188, 9, 24, 1, 25, 1, 25, 5, 25, 192, 8, 25,
10, 25, 12, 25, 195, 9, 25, 1, 25, 1, 25, 1, 25, 5, 25, 200, 8, 25, 10,
25, 12, 25, 203, 9, 25, 1, 25, 3, 25, 206, 8, 25, 1, 26, 4, 26, 209, 8,
26, 11, 26, 12, 26, 210, 1, 26, 1, 26, 1, 27, 1, 27, 0, 0, 28, 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, 20, 41, 21,
43, 22, 45, 0, 47, 23, 49, 24, 51, 25, 53, 26, 55, 0, 1, 0, 19, 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, 73, 73,
105, 105, 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, 1, 0, 39, 39, 1, 0, 34, 34, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48,
57, 233, 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, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 47, 1, 0,
0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 1, 57, 1,
0, 0, 0, 3, 59, 1, 0, 0, 0, 5, 61, 1, 0, 0, 0, 7, 63, 1, 0, 0, 0, 9, 65,
1, 0, 0, 0, 11, 70, 1, 0, 0, 0, 13, 72, 1, 0, 0, 0, 15, 75, 1, 0, 0, 0,
17, 78, 1, 0, 0, 0, 19, 80, 1, 0, 0, 0, 21, 83, 1, 0, 0, 0, 23, 85, 1,
0, 0, 0, 25, 88, 1, 0, 0, 0, 27, 90, 1, 0, 0, 0, 29, 92, 1, 0, 0, 0, 31,
94, 1, 0, 0, 0, 33, 96, 1, 0, 0, 0, 35, 98, 1, 0, 0, 0, 37, 102, 1, 0,
0, 0, 39, 106, 1, 0, 0, 0, 41, 109, 1, 0, 0, 0, 43, 121, 1, 0, 0, 0, 45,
123, 1, 0, 0, 0, 47, 167, 1, 0, 0, 0, 49, 169, 1, 0, 0, 0, 51, 205, 1,
0, 0, 0, 53, 208, 1, 0, 0, 0, 55, 214, 1, 0, 0, 0, 57, 58, 5, 40, 0, 0,
58, 2, 1, 0, 0, 0, 59, 60, 5, 41, 0, 0, 60, 4, 1, 0, 0, 0, 61, 62, 5, 91,
0, 0, 62, 6, 1, 0, 0, 0, 63, 64, 5, 93, 0, 0, 64, 8, 1, 0, 0, 0, 65, 66,
5, 44, 0, 0, 66, 10, 1, 0, 0, 0, 67, 71, 5, 61, 0, 0, 68, 69, 5, 61, 0,
0, 69, 71, 5, 61, 0, 0, 70, 67, 1, 0, 0, 0, 70, 68, 1, 0, 0, 0, 71, 12,
1, 0, 0, 0, 72, 73, 5, 33, 0, 0, 73, 74, 5, 61, 0, 0, 74, 14, 1, 0, 0,
0, 75, 76, 5, 60, 0, 0, 76, 77, 5, 62, 0, 0, 77, 16, 1, 0, 0, 0, 78, 79,
5, 60, 0, 0, 79, 18, 1, 0, 0, 0, 80, 81, 5, 60, 0, 0, 81, 82, 5, 61, 0,
0, 82, 20, 1, 0, 0, 0, 83, 84, 5, 62, 0, 0, 84, 22, 1, 0, 0, 0, 85, 86,
5, 62, 0, 0, 86, 87, 5, 61, 0, 0, 87, 24, 1, 0, 0, 0, 88, 89, 5, 43, 0,
0, 89, 26, 1, 0, 0, 0, 90, 91, 5, 45, 0, 0, 91, 28, 1, 0, 0, 0, 92, 93,
5, 42, 0, 0, 93, 30, 1, 0, 0, 0, 94, 95, 5, 47, 0, 0, 95, 32, 1, 0, 0,
0, 96, 97, 5, 37, 0, 0, 97, 34, 1, 0, 0, 0, 98, 99, 7, 0, 0, 0, 99, 100,
7, 1, 0, 0, 100, 101, 7, 2, 0, 0, 101, 36, 1, 0, 0, 0, 102, 103, 7, 3,
0, 0, 103, 104, 7, 0, 0, 0, 104, 105, 7, 4, 0, 0, 105, 38, 1, 0, 0, 0,
106, 107, 7, 1, 0, 0, 107, 108, 7, 5, 0, 0, 108, 40, 1, 0, 0, 0, 109, 110,
7, 6, 0, 0, 110, 111, 7, 0, 0, 0, 111, 42, 1, 0, 0, 0, 112, 113, 7, 2,
0, 0, 113, 114, 7, 5, 0, 0, 114, 115, 7, 7, 0, 0, 115, 122, 7, 8, 0, 0,
116, 117, 7, 9, 0, 0, 117, 118, 7, 3, 0, 0, 118, 119, 7, 10, 0, 0, 119,
120, 7, 11, 0, 0, 120, 122, 7, 8, 0, 0, 121, 112, 1, 0, 0, 0, 121, 116,
1, 0, 0, 0, 122, 44, 1, 0, 0, 0, 123, 124, 7, 12, 0, 0, 124, 46, 1, 0,
0, 0, 125, 127, 3, 55, 27, 0, 126, 125, 1, 0, 0, 0, 127, 128, 1, 0, 0,
0, 128, 126, 1, 0, 0, 0, 128, 129, 1, 0, 0, 0, 129, 137, 1, 0, 0, 0, 130,
134, 5, 46, 0, 0, 131, 133, 3, 55, 27, 0, 132, 131, 1, 0, 0, 0, 133, 136,
1, 0, 0, 0, 134, 132, 1, 0, 0, 0, 134, 135, 1, 0, 0, 0, 135, 138, 1, 0,
0, 0, 136, 134, 1, 0, 0, 0, 137, 130, 1, 0, 0, 0, 137, 138, 1, 0, 0, 0,
138, 148, 1, 0, 0, 0, 139, 141, 7, 8, 0, 0, 140, 142, 3, 45, 22, 0, 141,
140, 1, 0, 0, 0, 141, 142, 1, 0, 0, 0, 142, 144, 1, 0, 0, 0, 143, 145,
3, 55, 27, 0, 144, 143, 1, 0, 0, 0, 145, 146, 1, 0, 0, 0, 146, 144, 1,
0, 0, 0, 146, 147, 1, 0, 0, 0, 147, 149, 1, 0, 0, 0, 148, 139, 1, 0, 0,
0, 148, 149, 1, 0, 0, 0, 149, 168, 1, 0, 0, 0, 150, 152, 5, 46, 0, 0, 151,
153, 3, 55, 27, 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, 165, 1, 0, 0, 0, 156, 158, 7, 8,
0, 0, 157, 159, 3, 45, 22, 0, 158, 157, 1, 0, 0, 0, 158, 159, 1, 0, 0,
0, 159, 161, 1, 0, 0, 0, 160, 162, 3, 55, 27, 0, 161, 160, 1, 0, 0, 0,
162, 163, 1, 0, 0, 0, 163, 161, 1, 0, 0, 0, 163, 164, 1, 0, 0, 0, 164,
166, 1, 0, 0, 0, 165, 156, 1, 0, 0, 0, 165, 166, 1, 0, 0, 0, 166, 168,
1, 0, 0, 0, 167, 126, 1, 0, 0, 0, 167, 150, 1, 0, 0, 0, 168, 48, 1, 0,
0, 0, 169, 173, 7, 13, 0, 0, 170, 172, 7, 14, 0, 0, 171, 170, 1, 0, 0,
0, 172, 175, 1, 0, 0, 0, 173, 171, 1, 0, 0, 0, 173, 174, 1, 0, 0, 0, 174,
186, 1, 0, 0, 0, 175, 173, 1, 0, 0, 0, 176, 177, 5, 46, 0, 0, 177, 181,
7, 13, 0, 0, 178, 180, 7, 14, 0, 0, 179, 178, 1, 0, 0, 0, 180, 183, 1,
0, 0, 0, 181, 179, 1, 0, 0, 0, 181, 182, 1, 0, 0, 0, 182, 185, 1, 0, 0,
0, 183, 181, 1, 0, 0, 0, 184, 176, 1, 0, 0, 0, 185, 188, 1, 0, 0, 0, 186,
184, 1, 0, 0, 0, 186, 187, 1, 0, 0, 0, 187, 50, 1, 0, 0, 0, 188, 186, 1,
0, 0, 0, 189, 193, 5, 39, 0, 0, 190, 192, 8, 15, 0, 0, 191, 190, 1, 0,
0, 0, 192, 195, 1, 0, 0, 0, 193, 191, 1, 0, 0, 0, 193, 194, 1, 0, 0, 0,
194, 196, 1, 0, 0, 0, 195, 193, 1, 0, 0, 0, 196, 206, 5, 39, 0, 0, 197,
201, 5, 34, 0, 0, 198, 200, 8, 16, 0, 0, 199, 198, 1, 0, 0, 0, 200, 203,
1, 0, 0, 0, 201, 199, 1, 0, 0, 0, 201, 202, 1, 0, 0, 0, 202, 204, 1, 0,
0, 0, 203, 201, 1, 0, 0, 0, 204, 206, 5, 34, 0, 0, 205, 189, 1, 0, 0, 0,
205, 197, 1, 0, 0, 0, 206, 52, 1, 0, 0, 0, 207, 209, 7, 17, 0, 0, 208,
207, 1, 0, 0, 0, 209, 210, 1, 0, 0, 0, 210, 208, 1, 0, 0, 0, 210, 211,
1, 0, 0, 0, 211, 212, 1, 0, 0, 0, 212, 213, 6, 26, 0, 0, 213, 54, 1, 0,
0, 0, 214, 215, 7, 18, 0, 0, 215, 56, 1, 0, 0, 0, 21, 0, 70, 121, 128,
134, 137, 141, 146, 148, 154, 158, 163, 165, 167, 173, 181, 186, 193, 201,
205, 210, 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
HavingExpressionLexerLBRACK = 3
HavingExpressionLexerRBRACK = 4
HavingExpressionLexerCOMMA = 5
HavingExpressionLexerEQUALS = 6
HavingExpressionLexerNOT_EQUALS = 7
HavingExpressionLexerNEQ = 8
HavingExpressionLexerLT = 9
HavingExpressionLexerLE = 10
HavingExpressionLexerGT = 11
HavingExpressionLexerGE = 12
HavingExpressionLexerPLUS = 13
HavingExpressionLexerMINUS = 14
HavingExpressionLexerSTAR = 15
HavingExpressionLexerSLASH = 16
HavingExpressionLexerPERCENT = 17
HavingExpressionLexerNOT = 18
HavingExpressionLexerAND = 19
HavingExpressionLexerOR = 20
HavingExpressionLexerIN = 21
HavingExpressionLexerBOOL = 22
HavingExpressionLexerNUMBER = 23
HavingExpressionLexerIDENTIFIER = 24
HavingExpressionLexerSTRING = 25
HavingExpressionLexerWS = 26
)
@@ -0,0 +1,118 @@
// 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)
// EnterInList is called when entering the inList production.
EnterInList(c *InListContext)
// EnterSignedNumber is called when entering the signedNumber production.
EnterSignedNumber(c *SignedNumberContext)
// 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)
// EnterFunctionArgList is called when entering the functionArgList production.
EnterFunctionArgList(c *FunctionArgListContext)
// EnterFuncArg is called when entering the funcArg production.
EnterFuncArg(c *FuncArgContext)
// EnterFuncArgToken is called when entering the funcArgToken production.
EnterFuncArgToken(c *FuncArgTokenContext)
// 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)
// ExitInList is called when exiting the inList production.
ExitInList(c *InListContext)
// ExitSignedNumber is called when exiting the signedNumber production.
ExitSignedNumber(c *SignedNumberContext)
// 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)
// ExitFunctionArgList is called when exiting the functionArgList production.
ExitFunctionArgList(c *FunctionArgListContext)
// ExitFuncArg is called when exiting the funcArg production.
ExitFuncArg(c *FuncArgContext)
// ExitFuncArgToken is called when exiting the funcArgToken production.
ExitFuncArgToken(c *FuncArgTokenContext)
// ExitIdentifier is called when exiting the identifier production.
ExitIdentifier(c *IdentifierContext)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
// 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#inList.
VisitInList(ctx *InListContext) interface{}
// Visit a parse tree produced by HavingExpressionParser#signedNumber.
VisitSignedNumber(ctx *SignedNumberContext) 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#functionArgList.
VisitFunctionArgList(ctx *FunctionArgListContext) interface{}
// Visit a parse tree produced by HavingExpressionParser#funcArg.
VisitFuncArg(ctx *FuncArgContext) interface{}
// Visit a parse tree produced by HavingExpressionParser#funcArgToken.
VisitFuncArgToken(ctx *FuncArgTokenContext) interface{}
// Visit a parse tree produced by HavingExpressionParser#identifier.
VisitIdentifier(ctx *IdentifierContext) interface{}
}
+42
View File
@@ -36,6 +36,7 @@ var friendly = map[string]string{
// literals / identifiers
"NUMBER": "number",
"STRING": "string",
"BOOL": "boolean",
"QUOTED_TEXT": "quoted text",
"KEY": "field name (ex: service.name)",
@@ -88,6 +89,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)
}
@@ -154,9 +157,48 @@ func (l *ErrorListener) SyntaxError(
}
}
// Check which "closing" tokens are in the expected set so we can suppress
// context-inappropriate tokens from the displayed set:
// - When RPAREN is expected, hide LBRACK/RBRACK (IN-list brackets confuse
// unclosed-paren errors, e.g. "{), [}" → "{)}")
// - When RBRACK is expected, hide COMMA (inside an IN bracket list the only
// meaningful fix is to close the bracket, e.g. "{,, ]}" → "{]}")
rparenType := pGetTokenType(p, "RPAREN")
lbrackType := pGetTokenType(p, "LBRACK")
rbrackType := pGetTokenType(p, "RBRACK")
commaType := pGetTokenType(p, "COMMA")
hasRParen, hasRBrack := false, false
for _, iv := range set.GetIntervals() {
for t := iv.Start; t <= iv.Stop; t++ {
if t == rparenType {
hasRParen = true
}
if t == rbrackType {
hasRBrack = true
}
}
}
uniq := map[string]struct{}{}
for _, iv := range set.GetIntervals() {
for t := iv.Start; t <= iv.Stop; t++ {
// Exclude the offending token itself from the expected set.
// ANTLR's error recovery sometimes leaves the offending token in
// the follow-set, producing a contradictory message like
// "expecting NOT but got 'NOT'".
if t == err.TokenType {
continue
}
// When RPAREN is expected, suppress LBRACK/RBRACK — they only
// appear in IN-list contexts and confuse unclosed-paren errors.
if hasRParen && (t == lbrackType || t == rbrackType) {
continue
}
// When RBRACK is expected (inside an IN bracket list), suppress
// COMMA — the only useful fix is closing the bracket.
if hasRBrack && t == commaType {
continue
}
sym := tokenName(p, t)
if sym == "KEY" {
if !hasValueLiterals {
@@ -0,0 +1,496 @@
package querybuilder
import (
"sort"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
grammar "github.com/SigNoz/signoz/pkg/parser/havingexpression/grammar"
"github.com/antlr4-go/antlr/v4"
"github.com/huandu/go-sqlbuilder"
)
// 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.
// The grammar now accepts complex function arguments (nested calls, string predicates),
// so all aggregation expression forms can be looked up directly via ctx.GetText().
// - STRING atoms (string literals in comparison position) set hasStringLiteral so a
// friendly "aggregator results are numeric" error can be returned.
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
hasStringLiteral bool
sb *sqlbuilder.SelectBuilder
}
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),
sb: sqlbuilder.NewSelectBuilder(),
}
}
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)
}
if len(parts) == 1 {
return parts[0]
}
return v.sb.Or(parts...)
}
// 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)
}
if len(parts) == 1 {
return parts[0]
}
return v.sb.And(parts...)
}
func (v *havingExpressionRewriteVisitor) visitPrimary(ctx grammar.IPrimaryContext) string {
if ctx.OrExpression() != nil {
inner := v.visitOrExpression(ctx.OrExpression())
if ctx.NOT() != nil {
return v.sb.Not(inner)
}
return v.sb.And(inner)
}
if ctx.Comparison() == nil {
return ""
}
inner := v.visitComparison(ctx.Comparison())
if ctx.NOT() != nil {
return v.sb.Not(inner)
}
return inner
}
func (v *havingExpressionRewriteVisitor) visitComparison(ctx grammar.IComparisonContext) string {
if ctx.IN() != nil {
if ctx.Operand(0) == nil || ctx.InList() == nil {
return ""
}
lhs := v.visitOperand(ctx.Operand(0))
signedNumbers := ctx.InList().AllSignedNumber()
vals := make([]interface{}, len(signedNumbers))
for i, n := range signedNumbers {
vals[i] = sqlbuilder.Raw(n.GetText())
}
if ctx.NOT() != nil {
// Here we need to compile because In generates lhs IN $1 syntax
sql, _ := v.sb.Args.CompileWithFlavor(v.sb.NotIn(lhs, vals...), sqlbuilder.ClickHouse)
return sql
}
// Here we need to compile because In generates lhs IN $1 syntax
sql, _ := v.sb.Args.CompileWithFlavor(v.sb.In(lhs, vals...), sqlbuilder.ClickHouse)
return sql
}
if ctx.CompOp() == nil || ctx.Operand(0) == nil || ctx.Operand(1) == nil {
return ""
}
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.Factor() != nil {
// Unary sign: (PLUS | MINUS) factor
sign := "+"
if ctx.MINUS() != nil {
sign = "-"
}
return sign + v.visitFactor(ctx.Factor())
}
if ctx.Operand() != nil {
return v.sb.And(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())
}
if ctx.STRING() != nil {
// String literals are never valid aggregation results; flag for a friendly error.
v.hasStringLiteral = true
return ctx.STRING().GetText()
}
text := ctx.NUMBER().GetText()
return text
}
// visitFunctionCall looks the full call text up in columnMap. WS tokens are skipped by
// the lexer, so ctx.GetText() returns the expression with all whitespace removed
// (e.g. "countIf(level='error')", "avg(sum(cpu_usage))", "count_distinct(a,b)").
// The column map stores both the original expression and a space-stripped version as
// keys, so the lookup is whitespace-insensitive regardless of how the user typed it.
// If not found, the function name is recorded as invalid.
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
}
// rewriteAndValidate is the single-pass implementation used by all RewriteFor* methods.
//
// Validation layers:
// 1. The visitor runs on the parse tree, rewriting and collecting invalid references.
// Unknown references (including unrecognised function calls) → lists valid references.
// The grammar now supports complex function arguments (nested calls, string predicates)
// so all aggregation expression forms are handled directly by the parser without any
// regex pre-substitution.
// 2. String literals in comparison-operand position → descriptive error
// ("aggregator results are numeric").
// 3. ANTLR syntax errors → error with messages referencing the original token names.
func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string, error) {
original := strings.TrimSpace(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 1 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)
// Layer 2 string literals in comparison-operand position (atom rule).
// The grammar accepts STRING tokens in atom so the parser can recover and continue,
// but the visitor flags them; aggregator results are always numeric.
// This is checked before invalid references so that "contains string literals" takes
// priority when a bare string literal is also an unresolvable operand.
if v.hasStringLiteral {
return "", errors.NewInvalidInputf(
errors.CodeInvalidInput,
"`Having` expression contains string literals",
).WithAdditional("Aggregator results are numeric")
}
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)
additional := []string{"Valid references are: [" + strings.Join(validKeys, ", ") + "]"}
if len(v.invalid) == 1 {
inv := v.invalid[0]
// Only suggest for plain identifier typos, not for unresolved function
// calls: a function call will appear as "name(" in the expression, and
// the closest valid key may itself contain "(" (e.g. "sum(a)"), making
// a simple string substitution produce a corrupt expression.
isFuncCall := strings.Contains(original, inv+"(")
if match, dist := closestMatch(inv, validKeys); !isFuncCall && !strings.Contains(match, "(") && dist <= 3 {
corrected := strings.ReplaceAll(original, inv, match)
additional = append(additional, "Suggestion: `"+corrected+"`")
}
}
return "", errors.NewInvalidInputf(
errors.CodeInvalidInput,
"Invalid references in `Having` expression: [%s]",
strings.Join(v.invalid, ", "),
).WithAdditional(additional...)
}
// 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"
}
additional := []string{detail}
// For single-error expressions, try to produce an actionable suggestion.
if len(allSyntaxErrors) == 1 {
if s := havingSuggestion(allSyntaxErrors[0], original); s != "" {
additional = append(additional, "Suggestion: `"+s+"`")
}
}
return "", errors.NewInvalidInputf(
errors.CodeInvalidInput,
"Syntax error in `Having` expression",
).WithAdditional(additional...)
}
return result, nil
}
// havingSuggestion returns a corrected expression string to show as a suggestion when
// the error matches a well-known single-mistake pattern, or "" when no suggestion
// can be formed. Only call this when there is exactly one syntax error.
//
// Recognised patterns (all produce a minimal, valid completion):
// 1. Bare aggregation — comparison operator expected at EOF: count() → count() > 0
// 2. Missing right operand after comparison op at EOF: count() > → count() > 0
// 3. Unclosed parenthesis — only ) expected at EOF: (total > 100 → (total > 100)
// 4. Dangling AND/OR at end of expression: total > 100 AND → total > 100
// 5. Leading OR at position 0: OR total > 100 → total > 100
func havingSuggestion(se *SyntaxErr, original string) string {
trimmed := strings.TrimSpace(original)
upper := strings.ToUpper(trimmed)
if se.TokenTxt == "EOF" {
// Pattern 4: dangling AND or OR at end of expression.
// e.g. total > 100 AND → total > 100
// Checked before Pattern 1 so that "expr AND" does not match Pattern 1.
if strings.HasSuffix(upper, " AND") {
return strings.TrimSpace(trimmed[:len(trimmed)-4])
}
if strings.HasSuffix(upper, " OR") {
return strings.TrimSpace(trimmed[:len(trimmed)-3])
}
// Pattern 1: bare aggregation reference — no comparison operator yet.
// Detected by: IDENTIFIER in expected (operand-continuation set), expression
// does not already end with a comparison operator (Pattern 2 handles that case),
// and no unclosed parenthesis (Pattern 3 handles that case).
// e.g. count() → count() > 0
// total_logs → total_logs > 0
if expectedContains(se, "IDENTIFIER") && !endsWithComparisonOp(trimmed) && !hasUnclosedParen(trimmed) {
return trimmed + " > 0"
}
// Pattern 2: comparison operator already written but right operand missing.
// e.g. count() > → count() > 0
if expectedContains(se, "number") && endsWithComparisonOp(trimmed) {
return trimmed + " 0"
}
// Pattern 3: unclosed parenthesis with content inside.
// e.g. (total > 100 AND count() < 500 → (total > 100 AND count() < 500)
// Guard len > 1 avoids a useless "()" suggestion for a bare "(".
if expectedContains(se, ")") && hasUnclosedParen(trimmed) && len(trimmed) > 1 {
return trimmed + ")"
}
// Pattern 6: unclosed IN bracket list.
// e.g. count() IN [1, 2, 3 → count() IN [1, 2, 3]
if expectedContains(se, "]") && hasUnclosedBracket(trimmed) && len(trimmed) > 1 {
return trimmed + "]"
}
return ""
}
// Pattern 5: leading OR at position 0.
// e.g. OR total > 100 → total > 100
if se.TokenTxt == "'OR'" && se.Col == 0 && strings.HasPrefix(upper, "OR ") {
return strings.TrimSpace(trimmed[3:])
}
return ""
}
// expectedContains reports whether label is present in se.Expected.
func expectedContains(se *SyntaxErr, label string) bool {
for _, e := range se.Expected {
if e == label {
return true
}
}
return false
}
// hasUnclosedParen reports whether s contains more '(' than ')'.
func hasUnclosedParen(s string) bool {
count := 0
for _, c := range s {
switch c {
case '(':
count++
case ')':
count--
}
}
return count > 0
}
// hasUnclosedBracket reports whether s contains more '[' than ']'.
func hasUnclosedBracket(s string) bool {
count := 0
for _, c := range s {
switch c {
case '[':
count++
case ']':
count--
}
}
return count > 0
}
// closestMatch returns the element of candidates with the smallest Levenshtein
// distance to query, along with that distance.
func closestMatch(query string, candidates []string) (string, int) {
best, bestDist := "", -1
for _, c := range candidates {
if d := levenshtein(query, c); bestDist < 0 || d < bestDist {
best, bestDist = c, d
}
}
return best, bestDist
}
// levenshtein computes the edit distance between a and b.
func levenshtein(a, b string) int {
ra, rb := []rune(a), []rune(b)
la, lb := len(ra), len(rb)
row := make([]int, lb+1)
for j := range row {
row[j] = j
}
for i := 1; i <= la; i++ {
prev := row[0]
row[0] = i
for j := 1; j <= lb; j++ {
tmp := row[j]
if ra[i-1] == rb[j-1] {
row[j] = prev
} else {
row[j] = 1 + min(prev, min(row[j], row[j-1]))
}
prev = tmp
}
}
return row[lb]
}
// endsWithComparisonOp reports whether s ends with a comparison operator token
// (longer operators are checked first to avoid ">=" being matched by ">").
func endsWithComparisonOp(s string) bool {
for _, op := range []string{">=", "<=", "!=", "<>", "==", ">", "<", "="} {
if strings.HasSuffix(s, op) {
return true
}
}
return false
}
+24 -56
View File
@@ -2,7 +2,6 @@ package querybuilder
import (
"fmt"
"regexp"
"strings"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
@@ -19,19 +18,31 @@ func NewHavingExpressionRewriter() *HavingExpressionRewriter {
}
}
func (r *HavingExpressionRewriter) RewriteForTraces(expression string, aggregations []qbtypes.TraceAggregation) string {
// RewriteForTraces rewrites and validates the HAVING expression for a traces query.
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 {
// RewriteForLogs rewrites and validates the HAVING expression for a logs query.
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 {
// RewriteForMetrics rewrites and validates the HAVING expression for a metrics query.
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) {
@@ -45,6 +56,9 @@ func (r *HavingExpressionRewriter) buildTraceColumnMap(aggregations []qbtypes.Tr
}
r.columnMap[agg.Expression] = sqlColumn
if normalized := strings.ReplaceAll(agg.Expression, " ", ""); normalized != agg.Expression {
r.columnMap[normalized] = sqlColumn
}
r.columnMap[fmt.Sprintf("__result%d", idx)] = sqlColumn
@@ -65,6 +79,9 @@ func (r *HavingExpressionRewriter) buildLogColumnMap(aggregations []qbtypes.LogA
}
r.columnMap[agg.Expression] = sqlColumn
if normalized := strings.ReplaceAll(agg.Expression, " ", ""); normalized != agg.Expression {
r.columnMap[normalized] = sqlColumn
}
r.columnMap[fmt.Sprintf("__result%d", idx)] = sqlColumn
@@ -102,52 +119,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
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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 -1
View File
@@ -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"
)
+1 -1
View File
@@ -9,7 +9,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"
@@ -7,7 +7,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"
+4 -4
View File
@@ -194,7 +194,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "ERROR: cannot execute update() in a read-only context",
shouldPass: false,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got ')'",
expectedErrorContains: "expecting one of {(, AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got ')'",
},
{
category: "Special characters",
@@ -616,7 +616,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
},
{
category: "Keyword conflict",
@@ -2016,7 +2016,7 @@ func TestFilterExprLogs(t *testing.T) {
expectedErrorContains: "",
},
{category: "Only keywords", query: "AND", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'AND'"},
{category: "Only keywords", query: "AND", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'AND'"},
{category: "Only keywords", query: "OR", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'OR'"},
{category: "Only keywords", query: "NOT", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got EOF"},
@@ -2160,7 +2160,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
expectedErrorContains: "line 1:0 expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
},
{
category: "Operator keywords as keys",
+12 -3
View File
@@ -427,7 +427,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)
}
@@ -453,7 +456,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)
}
@@ -560,7 +566,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)
}
+28 -5
View File
@@ -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",
@@ -30,7 +32,7 @@ func TestHavingExpressionRewriter_LogQueries(t *testing.T) {
{Expression: "count()", Alias: "total"},
{Expression: "avg(duration)", Alias: "avg_duration"},
},
expectedExpression: "(__result_0 > 100 AND __result_1 < 500) OR __result_0 > 10000",
expectedExpression: "(((__result_0 > 100 AND __result_1 < 500)) OR __result_0 > 10000)",
},
{
name: "__result reference for single aggregation",
@@ -55,7 +57,7 @@ func TestHavingExpressionRewriter_LogQueries(t *testing.T) {
{Expression: "count()", Alias: ""},
{Expression: "sum(bytes)", Alias: ""},
},
expectedExpression: "__result_0 > 100 AND __result_1 < 1000",
expectedExpression: "(__result_0 > 100 AND __result_1 < 1000)",
},
{
name: "mixed aliases and expressions",
@@ -64,15 +66,36 @@ func TestHavingExpressionRewriter_LogQueries(t *testing.T) {
{Expression: "count()", Alias: ""},
{Expression: "countIf(level='error')", Alias: "error_count"},
},
expectedExpression: "__result_1 > 10 AND __result_0 < 1000",
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)
}
})
}
}
+12 -3
View File
@@ -574,7 +574,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 {
@@ -597,7 +600,10 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
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 {
@@ -606,7 +612,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)
}
}
+12 -3
View File
@@ -552,7 +552,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)
}
@@ -578,7 +581,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)
}
@@ -684,7 +690,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)
}
+1 -1
View File
@@ -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"
)
@@ -624,7 +624,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{
@@ -740,7 +743,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 {
@@ -883,7 +889,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{
@@ -892,12 +901,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) {
@@ -264,6 +264,12 @@ func (q *QueryBuilderQuery[T]) validateAggregations(cfg validationConfig) error
}
aliases[v.Alias] = true
}
if strings.Contains(strings.ToLower(v.Expression), " as ") {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"aliasing is not allowed in expression. Use `alias` field instead",
)
}
case LogAggregation:
if v.Expression == "" {
aggId := fmt.Sprintf("aggregation #%d", i+1)
@@ -286,6 +292,12 @@ func (q *QueryBuilderQuery[T]) validateAggregations(cfg validationConfig) error
}
aliases[v.Alias] = true
}
if strings.Contains(strings.ToLower(v.Expression), " as ") {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"aliasing is not allowed in expression. Use `alias` field instead",
)
}
}
}
+1 -1
View File
@@ -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"
)
+2 -1
View File
@@ -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"