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) } }