signoz/pkg/variables/variable_replace_visitor.go
Nityananda Gohain 514bceca34
feat: support for hasToken (#8891)
* feat: support for hasToken

* fix: address comments

* fix: address comments
2025-08-26 05:58:31 +00:00

533 lines
14 KiB
Go

package variables
import (
"fmt"
"strconv"
"strings"
grammar "github.com/SigNoz/signoz/pkg/parser/grammar"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/antlr4-go/antlr/v4"
)
// ErrorListener collects syntax errors during parsing
type ErrorListener struct {
*antlr.DefaultErrorListener
SyntaxErrors []error
}
// NewErrorListener creates a new error listener
func NewErrorListener() *ErrorListener {
return &ErrorListener{
DefaultErrorListener: antlr.NewDefaultErrorListener(),
SyntaxErrors: []error{},
}
}
// SyntaxError is called when a syntax error is encountered
func (e *ErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol any, line, column int, msg string, ex antlr.RecognitionException) {
e.SyntaxErrors = append(e.SyntaxErrors, fmt.Errorf("line %d:%d %s", line, column, msg))
}
// variableReplacementVisitor implements the visitor interface
// to replace variables in filter expressions with their actual values
type variableReplacementVisitor struct {
variables map[string]qbtypes.VariableItem
errors []string
}
// specialSkipMarker is used to indicate that a condition should be removed
const specialSkipMarker = "__SKIP_CONDITION__"
// ReplaceVariablesInExpression takes a filter expression and returns it with variables replaced
func ReplaceVariablesInExpression(expression string, variables map[string]qbtypes.VariableItem) (string, error) {
input := antlr.NewInputStream(expression)
lexer := grammar.NewFilterQueryLexer(input)
visitor := &variableReplacementVisitor{
variables: variables,
errors: []string{},
}
lexerErrorListener := NewErrorListener()
lexer.RemoveErrorListeners()
lexer.AddErrorListener(lexerErrorListener)
tokens := antlr.NewCommonTokenStream(lexer, 0)
parserErrorListener := NewErrorListener()
parser := grammar.NewFilterQueryParser(tokens)
parser.RemoveErrorListeners()
parser.AddErrorListener(parserErrorListener)
tree := parser.Query()
if len(parserErrorListener.SyntaxErrors) > 0 {
return "", fmt.Errorf("syntax errors in expression: %v", parserErrorListener.SyntaxErrors)
}
result := visitor.Visit(tree).(string)
if len(visitor.errors) > 0 {
return "", fmt.Errorf("errors processing expression: %v", visitor.errors)
}
// If the entire expression should be skipped, return empty string
if result == specialSkipMarker {
return "", nil
}
return result, nil
}
// Visit dispatches to the specific visit method based on node type
func (v *variableReplacementVisitor) Visit(tree antlr.ParseTree) any {
if tree == nil {
return ""
}
switch t := tree.(type) {
case *grammar.QueryContext:
return v.VisitQuery(t)
case *grammar.ExpressionContext:
return v.VisitExpression(t)
case *grammar.OrExpressionContext:
return v.VisitOrExpression(t)
case *grammar.AndExpressionContext:
return v.VisitAndExpression(t)
case *grammar.UnaryExpressionContext:
return v.VisitUnaryExpression(t)
case *grammar.PrimaryContext:
return v.VisitPrimary(t)
case *grammar.ComparisonContext:
return v.VisitComparison(t)
case *grammar.InClauseContext:
return v.VisitInClause(t)
case *grammar.NotInClauseContext:
return v.VisitNotInClause(t)
case *grammar.ValueListContext:
return v.VisitValueList(t)
case *grammar.FullTextContext:
return v.VisitFullText(t)
case *grammar.FunctionCallContext:
return v.VisitFunctionCall(t)
case *grammar.FunctionParamListContext:
return v.VisitFunctionParamList(t)
case *grammar.FunctionParamContext:
return v.VisitFunctionParam(t)
case *grammar.ArrayContext:
return v.VisitArray(t)
case *grammar.ValueContext:
return v.VisitValue(t)
case *grammar.KeyContext:
return v.VisitKey(t)
default:
// For unknown types, return the original text
return tree.GetText()
}
}
func (v *variableReplacementVisitor) VisitQuery(ctx *grammar.QueryContext) any {
return v.Visit(ctx.Expression())
}
func (v *variableReplacementVisitor) VisitExpression(ctx *grammar.ExpressionContext) any {
return v.Visit(ctx.OrExpression())
}
func (v *variableReplacementVisitor) VisitOrExpression(ctx *grammar.OrExpressionContext) any {
andExpressions := ctx.AllAndExpression()
parts := make([]string, 0, len(andExpressions))
for _, expr := range andExpressions {
part := v.Visit(expr).(string)
// Skip conditions that should be removed
if part != specialSkipMarker && part != "" {
parts = append(parts, part)
}
}
if len(parts) == 0 {
return specialSkipMarker
}
if len(parts) == 1 {
return parts[0]
}
return strings.Join(parts, " OR ")
}
func (v *variableReplacementVisitor) VisitAndExpression(ctx *grammar.AndExpressionContext) any {
unaryExpressions := ctx.AllUnaryExpression()
parts := make([]string, 0, len(unaryExpressions))
for _, expr := range unaryExpressions {
part := v.Visit(expr).(string)
// Skip conditions that should be removed
if part != specialSkipMarker && part != "" {
parts = append(parts, part)
}
}
if len(parts) == 0 {
return specialSkipMarker
}
if len(parts) == 1 {
return parts[0]
}
return strings.Join(parts, " AND ")
}
func (v *variableReplacementVisitor) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) any {
result := v.Visit(ctx.Primary()).(string)
// If the condition should be skipped, propagate it up
if result == specialSkipMarker {
return specialSkipMarker
}
if ctx.NOT() != nil {
return "NOT " + result
}
return result
}
func (v *variableReplacementVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any {
if ctx.OrExpression() != nil {
return "(" + v.Visit(ctx.OrExpression()).(string) + ")"
} else if ctx.Comparison() != nil {
return v.Visit(ctx.Comparison())
} else if ctx.FunctionCall() != nil {
return v.Visit(ctx.FunctionCall())
} else if ctx.FullText() != nil {
return v.Visit(ctx.FullText())
}
// Handle standalone key/value
if ctx.GetChildCount() == 1 {
child := ctx.GetChild(0)
if parseTree, ok := child.(antlr.ParseTree); ok {
return v.Visit(parseTree).(string)
}
// Fallback to getting text from the context
return ctx.GetText()
}
return ctx.GetText()
}
func (v *variableReplacementVisitor) VisitComparison(ctx *grammar.ComparisonContext) any {
// First check if any value contains __all__ variable
values := ctx.AllValue()
for _, val := range values {
valueResult := v.Visit(val).(string)
if valueResult == specialSkipMarker {
return specialSkipMarker
}
}
// Also check in IN/NOT IN clauses
if ctx.InClause() != nil {
inResult := v.Visit(ctx.InClause()).(string)
if inResult == specialSkipMarker {
return specialSkipMarker
}
}
if ctx.NotInClause() != nil {
notInResult := v.Visit(ctx.NotInClause()).(string)
if notInResult == specialSkipMarker {
return specialSkipMarker
}
}
var parts []string
// Add key
parts = append(parts, v.Visit(ctx.Key()).(string))
// Handle EXISTS
if ctx.EXISTS() != nil {
if ctx.NOT() != nil {
parts = append(parts, " NOT")
}
parts = append(parts, " EXISTS")
return strings.Join(parts, "")
}
// Handle IN/NOT IN
if ctx.InClause() != nil {
parts = append(parts, " IN ")
parts = append(parts, v.Visit(ctx.InClause()).(string))
return strings.Join(parts, "")
}
if ctx.NotInClause() != nil {
parts = append(parts, " NOT IN ")
parts = append(parts, v.Visit(ctx.NotInClause()).(string))
return strings.Join(parts, "")
}
// Handle BETWEEN
if ctx.BETWEEN() != nil {
if ctx.NOT() != nil {
parts = append(parts, " NOT")
}
parts = append(parts, " BETWEEN ")
values := ctx.AllValue()
parts = append(parts, v.Visit(values[0]).(string))
parts = append(parts, " AND ")
parts = append(parts, v.Visit(values[1]).(string))
return strings.Join(parts, "")
}
// Handle other operators
if ctx.EQUALS() != nil {
parts = append(parts, " = ")
} else if ctx.NOT_EQUALS() != nil {
parts = append(parts, " != ")
} else if ctx.NEQ() != nil {
parts = append(parts, " <> ")
} else if ctx.LT() != nil {
parts = append(parts, " < ")
} else if ctx.LE() != nil {
parts = append(parts, " <= ")
} else if ctx.GT() != nil {
parts = append(parts, " > ")
} else if ctx.GE() != nil {
parts = append(parts, " >= ")
} else if ctx.LIKE() != nil {
if ctx.NOT() != nil {
parts = append(parts, " NOT")
}
parts = append(parts, " LIKE ")
} else if ctx.ILIKE() != nil {
if ctx.NOT() != nil {
parts = append(parts, " NOT")
}
parts = append(parts, " ILIKE ")
} else if ctx.REGEXP() != nil {
if ctx.NOT() != nil {
parts = append(parts, " NOT")
}
parts = append(parts, " REGEXP ")
} else if ctx.CONTAINS() != nil {
if ctx.NOT() != nil {
parts = append(parts, " NOT")
}
parts = append(parts, " CONTAINS ")
}
// Add value
if len(values) > 0 {
parts = append(parts, v.Visit(values[0]).(string))
}
return strings.Join(parts, "")
}
func (v *variableReplacementVisitor) VisitInClause(ctx *grammar.InClauseContext) any {
if ctx.ValueList() != nil {
result := v.Visit(ctx.ValueList()).(string)
if result == specialSkipMarker {
return specialSkipMarker
}
return result
}
result := v.Visit(ctx.Value()).(string)
if result == specialSkipMarker {
return specialSkipMarker
}
return result
}
func (v *variableReplacementVisitor) VisitNotInClause(ctx *grammar.NotInClauseContext) any {
if ctx.ValueList() != nil {
result := v.Visit(ctx.ValueList()).(string)
if result == specialSkipMarker {
return specialSkipMarker
}
return result
}
result := v.Visit(ctx.Value()).(string)
if result == specialSkipMarker {
return specialSkipMarker
}
return result
}
func (v *variableReplacementVisitor) VisitValueList(ctx *grammar.ValueListContext) any {
values := ctx.AllValue()
// Check if any value is __all__
for _, val := range values {
result := v.Visit(val).(string)
if result == specialSkipMarker {
return specialSkipMarker
}
}
parts := make([]string, 0, len(values))
for i, val := range values {
if i > 0 {
parts = append(parts, ", ")
}
parts = append(parts, v.Visit(val).(string))
}
return "(" + strings.Join(parts, "") + ")"
}
func (v *variableReplacementVisitor) VisitFullText(ctx *grammar.FullTextContext) any {
if ctx.QUOTED_TEXT() != nil {
return ctx.QUOTED_TEXT().GetText()
} else if ctx.FREETEXT() != nil {
return ctx.FREETEXT().GetText()
}
return ""
}
func (v *variableReplacementVisitor) VisitFunctionCall(ctx *grammar.FunctionCallContext) any {
var functionName string
if ctx.HAS() != nil {
functionName = "has"
} else if ctx.HASANY() != nil {
functionName = "hasAny"
} else if ctx.HASALL() != nil {
functionName = "hasAll"
} else if ctx.HASTOKEN() != nil {
functionName = "hasToken"
}
params := v.Visit(ctx.FunctionParamList()).(string)
return functionName + "(" + params + ")"
}
func (v *variableReplacementVisitor) VisitFunctionParamList(ctx *grammar.FunctionParamListContext) any {
params := ctx.AllFunctionParam()
parts := make([]string, 0, len(params))
for i, param := range params {
if i > 0 {
parts = append(parts, ", ")
}
parts = append(parts, v.Visit(param).(string))
}
return strings.Join(parts, "")
}
func (v *variableReplacementVisitor) VisitFunctionParam(ctx *grammar.FunctionParamContext) any {
if ctx.Key() != nil {
return v.Visit(ctx.Key())
} else if ctx.Value() != nil {
return v.Visit(ctx.Value())
} else if ctx.Array() != nil {
return v.Visit(ctx.Array())
}
return ""
}
func (v *variableReplacementVisitor) VisitArray(ctx *grammar.ArrayContext) any {
valueList := v.Visit(ctx.ValueList()).(string)
// Don't wrap in brackets if it's already wrapped in parentheses
if strings.HasPrefix(valueList, "(") {
return valueList
}
return "[" + valueList + "]"
}
func (v *variableReplacementVisitor) VisitValue(ctx *grammar.ValueContext) any {
// First get the original value
var originalValue string
if ctx.QUOTED_TEXT() != nil {
originalValue = ctx.QUOTED_TEXT().GetText()
} else if ctx.NUMBER() != nil {
originalValue = ctx.NUMBER().GetText()
} else if ctx.KEY() != nil {
originalValue = ctx.KEY().GetText()
}
// Check if this is a variable (starts with $)
if strings.HasPrefix(originalValue, "$") {
varName := originalValue
// Try with $ prefix first
varItem, ok := v.variables[varName]
if !ok && len(varName) > 1 {
// Try without $ prefix
varItem, ok = v.variables[varName[1:]]
}
if ok {
// Handle dynamic variable with __all__ value
if varItem.Type == qbtypes.DynamicVariableType {
if allVal, ok := varItem.Value.(string); ok && allVal == "__all__" {
// Return special marker to indicate this condition should be removed
return specialSkipMarker
}
}
// Replace variable with its value
return v.formatVariableValue(varItem.Value)
}
}
// Return original value if not a variable or variable not found
return originalValue
}
func (v *variableReplacementVisitor) VisitKey(ctx *grammar.KeyContext) any {
keyText := ctx.GetText()
// Check if this key is actually a variable
if strings.HasPrefix(keyText, "$") {
varName := keyText
// Try with $ prefix first
varItem, ok := v.variables[varName]
if !ok && len(varName) > 1 {
// Try without $ prefix
varItem, ok = v.variables[varName[1:]]
}
if ok {
// Handle dynamic variable with __all__ value
if varItem.Type == qbtypes.DynamicVariableType {
if allVal, ok := varItem.Value.(string); ok && allVal == "__all__" {
return specialSkipMarker
}
}
// Replace variable with its value
return v.formatVariableValue(varItem.Value)
}
}
return keyText
}
// formatVariableValue formats a variable value for inclusion in the expression
func (v *variableReplacementVisitor) formatVariableValue(value any) string {
switch val := value.(type) {
case string:
// Quote string values
return fmt.Sprintf("'%s'", strings.ReplaceAll(val, "'", "\\'"))
case []any:
// Format array values
parts := make([]string, len(val))
for i, item := range val {
parts[i] = v.formatVariableValue(item)
}
return "(" + strings.Join(parts, ", ") + ")"
case int, int32, int64, float32, float64:
return fmt.Sprintf("%v", val)
case bool:
return strconv.FormatBool(val)
default:
return fmt.Sprintf("%v", val)
}
}