package contextlinks import ( "fmt" "slices" "strings" parser "github.com/SigNoz/signoz/pkg/parser/grammar" "github.com/antlr4-go/antlr/v4" "golang.org/x/exp/maps" qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" ) type WhereClauseRewriter struct { parser.BaseFilterQueryVisitor labels map[string]string groupByItems []qbtypes.GroupByKey groupBySet map[string]struct{} keysSeen map[string]struct{} rewritten strings.Builder } // PrepareFilterExpression prepares the where clause for the query // `labels` contains the key value pairs of the labels from the result of the query // We "visit" the where clause and make necessary changes to existing query // There are two cases: // 1. The label is present in the where clause // 2. The label is not present in the where clause // // Example for case 2: // Latency by service.name without any filter // In this case, for each service with latency > threshold we send a notification // The expectation is that clicking on the related traces for service A, will // take us to the traces page with the filter service.name=A // So for all the missing labels in the where clause, we add them as key = value // // Example for case 1: // Severity text IN (WARN, ERROR) // In this case, the Severity text will appear in the `labels` if it were part of the group // by clause, in which case we replace it with the actual value for the notification // i.e Severity text = WARN // If the Severity text is not part of the group by clause, then we add it as it is func PrepareFilterExpression(labels map[string]string, whereClause string, groupByItems []qbtypes.GroupByKey) string { if whereClause == "" && len(labels) == 0 { return "" } //delete predefined alert labels for _, label := range PredefinedAlertLabels { delete(labels, label) } groupBySet := make(map[string]struct{}) for _, item := range groupByItems { groupBySet[item.Name] = struct{}{} } input := antlr.NewInputStream(whereClause) lexer := parser.NewFilterQueryLexer(input) stream := antlr.NewCommonTokenStream(lexer, 0) parser := parser.NewFilterQueryParser(stream) tree := parser.Query() rewriter := &WhereClauseRewriter{ labels: labels, groupByItems: groupByItems, groupBySet: groupBySet, keysSeen: map[string]struct{}{}, } // visit the tree to rewrite the where clause rewriter.Visit(tree) rewrittenClause := strings.TrimSpace(rewriter.rewritten.String()) // sorted key for deterministic order sortedKeys := maps.Keys(labels) slices.Sort(sortedKeys) // case 2: add missing labels from the labels map missingLabels := []string{} for _, key := range sortedKeys { if !rewriter.isKeyInWhereClause(key) { // escape the value if it contains special characters or spaces escapedValue := escapeValueIfNeeded(labels[key]) missingLabels = append(missingLabels, fmt.Sprintf("%s=%s", key, escapedValue)) } } // combine if len(missingLabels) > 0 { if rewrittenClause != "" { rewrittenClause = fmt.Sprintf("(%s) AND %s", rewrittenClause, strings.Join(missingLabels, " AND ")) } else { rewrittenClause = strings.Join(missingLabels, " AND ") } } return rewrittenClause } // Visit implements the visitor for the query rule func (r *WhereClauseRewriter) Visit(tree antlr.ParseTree) interface{} { return tree.Accept(r) } // VisitQuery visits the query node func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) interface{} { if ctx.Expression() != nil { ctx.Expression().Accept(r) } return nil } // VisitExpression visits the expression node func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) interface{} { if ctx.OrExpression() != nil { ctx.OrExpression().Accept(r) } return nil } // VisitOrExpression visits OR expressions func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext) interface{} { for i, andExpr := range ctx.AllAndExpression() { if i > 0 { r.rewritten.WriteString(" OR ") } andExpr.Accept(r) } return nil } // VisitAndExpression visits AND expressions func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContext) interface{} { unaryExprs := ctx.AllUnaryExpression() for i, unaryExpr := range unaryExprs { if i > 0 { // Check if there's an explicit AND if i-1 < len(ctx.AllAND()) && ctx.AND(i-1) != nil { r.rewritten.WriteString(" AND ") } else { // implicit r.rewritten.WriteString(" AND ") } } unaryExpr.Accept(r) } return nil } // VisitUnaryExpression visits unary expressions (with optional NOT) func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionContext) interface{} { if ctx.NOT() != nil { r.rewritten.WriteString("NOT ") } if ctx.Primary() != nil { ctx.Primary().Accept(r) } return nil } // VisitPrimary visits primary expressions func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) interface{} { if ctx.LPAREN() != nil && ctx.RPAREN() != nil { r.rewritten.WriteString("(") if ctx.OrExpression() != nil { ctx.OrExpression().Accept(r) } r.rewritten.WriteString(")") } else if ctx.Comparison() != nil { ctx.Comparison().Accept(r) } else if ctx.FunctionCall() != nil { ctx.FunctionCall().Accept(r) } else if ctx.FullText() != nil { ctx.FullText().Accept(r) } else if ctx.Key() != nil { ctx.Key().Accept(r) } else if ctx.Value() != nil { ctx.Value().Accept(r) } return nil } // VisitComparison visits comparison expressions func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) interface{} { if ctx.Key() == nil { return nil } key := ctx.Key().GetText() r.keysSeen[key] = struct{}{} // Check if this key is in the labels and was part of group by if value, exists := r.labels[key]; exists { if _, partOfGroup := r.groupBySet[key]; partOfGroup { // Case 1: Replace with actual value escapedValue := escapeValueIfNeeded(value) r.rewritten.WriteString(fmt.Sprintf("%s=%s", key, escapedValue)) return nil } } // Otherwise, keep the original comparison r.rewritten.WriteString(key) if ctx.EQUALS() != nil { r.rewritten.WriteString("=") if ctx.Value(0) != nil { r.rewritten.WriteString(ctx.Value(0).GetText()) } } else if ctx.NOT_EQUALS() != nil || ctx.NEQ() != nil { if ctx.NOT_EQUALS() != nil { r.rewritten.WriteString("!=") } else { r.rewritten.WriteString("<>") } if ctx.Value(0) != nil { r.rewritten.WriteString(ctx.Value(0).GetText()) } } else if ctx.LT() != nil { r.rewritten.WriteString("<") if ctx.Value(0) != nil { r.rewritten.WriteString(ctx.Value(0).GetText()) } } else if ctx.LE() != nil { r.rewritten.WriteString("<=") if ctx.Value(0) != nil { r.rewritten.WriteString(ctx.Value(0).GetText()) } } else if ctx.GT() != nil { r.rewritten.WriteString(">") if ctx.Value(0) != nil { r.rewritten.WriteString(ctx.Value(0).GetText()) } } else if ctx.GE() != nil { r.rewritten.WriteString(">=") if ctx.Value(0) != nil { r.rewritten.WriteString(ctx.Value(0).GetText()) } } else if ctx.LIKE() != nil || ctx.ILIKE() != nil { if ctx.LIKE() != nil { if ctx.NOT() != nil { r.rewritten.WriteString(" NOT LIKE ") } else { r.rewritten.WriteString(" LIKE ") } } else { if ctx.NOT() != nil { r.rewritten.WriteString(" NOT ILIKE ") } else { r.rewritten.WriteString(" ILIKE ") } } if ctx.Value(0) != nil { r.rewritten.WriteString(ctx.Value(0).GetText()) } } else if ctx.BETWEEN() != nil { if ctx.NOT() != nil { r.rewritten.WriteString(" NOT BETWEEN ") } else { r.rewritten.WriteString(" BETWEEN ") } if len(ctx.AllValue()) >= 2 { r.rewritten.WriteString(ctx.Value(0).GetText()) r.rewritten.WriteString(" AND ") r.rewritten.WriteString(ctx.Value(1).GetText()) } } else if ctx.InClause() != nil { r.rewritten.WriteString(" ") ctx.InClause().Accept(r) } else if ctx.NotInClause() != nil { r.rewritten.WriteString(" ") ctx.NotInClause().Accept(r) } else if ctx.EXISTS() != nil { if ctx.NOT() != nil { r.rewritten.WriteString(" NOT EXISTS") } else { r.rewritten.WriteString(" EXISTS") } } else if ctx.REGEXP() != nil { if ctx.NOT() != nil { r.rewritten.WriteString(" NOT REGEXP ") } else { r.rewritten.WriteString(" REGEXP ") } if ctx.Value(0) != nil { r.rewritten.WriteString(ctx.Value(0).GetText()) } } else if ctx.CONTAINS() != nil { if ctx.NOT() != nil { r.rewritten.WriteString(" NOT CONTAINS ") } else { r.rewritten.WriteString(" CONTAINS ") } if ctx.Value(0) != nil { r.rewritten.WriteString(ctx.Value(0).GetText()) } } return nil } // VisitInClause visits IN clauses func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) interface{} { r.rewritten.WriteString("IN ") if ctx.LPAREN() != nil { r.rewritten.WriteString("(") if ctx.ValueList() != nil { ctx.ValueList().Accept(r) } r.rewritten.WriteString(")") } else if ctx.LBRACK() != nil { r.rewritten.WriteString("[") if ctx.ValueList() != nil { ctx.ValueList().Accept(r) } r.rewritten.WriteString("]") } else if ctx.Value() != nil { r.rewritten.WriteString(ctx.Value().GetText()) } return nil } // VisitNotInClause visits NOT IN clauses func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) interface{} { r.rewritten.WriteString("NOT IN ") if ctx.LPAREN() != nil { r.rewritten.WriteString("(") if ctx.ValueList() != nil { ctx.ValueList().Accept(r) } r.rewritten.WriteString(")") } else if ctx.LBRACK() != nil { r.rewritten.WriteString("[") if ctx.ValueList() != nil { ctx.ValueList().Accept(r) } r.rewritten.WriteString("]") } else if ctx.Value() != nil { r.rewritten.WriteString(ctx.Value().GetText()) } return nil } // VisitValueList visits value lists func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) interface{} { values := ctx.AllValue() for i, val := range values { if i > 0 { r.rewritten.WriteString(", ") } r.rewritten.WriteString(val.GetText()) } return nil } // VisitFullText visits full text expressions func (r *WhereClauseRewriter) VisitFullText(ctx *parser.FullTextContext) interface{} { r.rewritten.WriteString(ctx.GetText()) return nil } // VisitFunctionCall visits function calls func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext) interface{} { // Write function name if ctx.HAS() != nil { r.rewritten.WriteString("has") } else if ctx.HASANY() != nil { r.rewritten.WriteString("hasany") } else if ctx.HASALL() != nil { r.rewritten.WriteString("hasall") } else if ctx.HASTOKEN() != nil { r.rewritten.WriteString("hasToken") } r.rewritten.WriteString("(") if ctx.FunctionParamList() != nil { ctx.FunctionParamList().Accept(r) } r.rewritten.WriteString(")") return nil } // VisitFunctionParamList visits function parameter lists func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamListContext) interface{} { params := ctx.AllFunctionParam() for i, param := range params { if i > 0 { r.rewritten.WriteString(", ") } param.Accept(r) } return nil } // VisitFunctionParam visits function parameters func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContext) interface{} { if ctx.Key() != nil { ctx.Key().Accept(r) } else if ctx.Value() != nil { ctx.Value().Accept(r) } else if ctx.Array() != nil { ctx.Array().Accept(r) } return nil } // VisitArray visits array expressions func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) interface{} { r.rewritten.WriteString("[") if ctx.ValueList() != nil { ctx.ValueList().Accept(r) } r.rewritten.WriteString("]") return nil } // VisitValue visits value expressions func (r *WhereClauseRewriter) VisitValue(ctx *parser.ValueContext) interface{} { r.rewritten.WriteString(ctx.GetText()) return nil } // VisitKey visits key expressions func (r *WhereClauseRewriter) VisitKey(ctx *parser.KeyContext) interface{} { r.keysSeen[ctx.GetText()] = struct{}{} r.rewritten.WriteString(ctx.GetText()) return nil } func (r *WhereClauseRewriter) isKeyInWhereClause(key string) bool { _, ok := r.keysSeen[key] return ok } // escapeValueIfNeeded adds single quotes to string values and escapes single quotes within them // Numeric and boolean values are returned as-is func escapeValueIfNeeded(value string) string { // Check if it's a number if _, err := fmt.Sscanf(value, "%f", new(float64)); err == nil { return value } // Check if it's a boolean if strings.ToLower(value) == "true" || strings.ToLower(value) == "false" { return value } // For all other values (strings), escape single quotes and wrap in single quotes escaped := strings.ReplaceAll(value, "'", "\\'") return fmt.Sprintf("'%s'", escaped) }