diff --git a/pkg/querybuilder/never_true.go b/pkg/querybuilder/never_true.go new file mode 100644 index 000000000000..22c4bcde5303 --- /dev/null +++ b/pkg/querybuilder/never_true.go @@ -0,0 +1,937 @@ +package querybuilder + +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" +) + +// FieldConstraint represents a constraint on a field +type FieldConstraint struct { + Field string + Operator qbtypes.FilterOperator + Value interface{} + Values []interface{} // For IN, NOT IN operations +} + +// ConstraintSet represents a set of constraints that must all be true (AND) +type ConstraintSet struct { + Constraints map[string][]FieldConstraint // field -> constraints +} + +// LogicalContradictionDetector implements the visitor pattern to detect logical contradictions +type LogicalContradictionDetector struct { + grammar.BaseFilterQueryVisitor + constraintStack []*ConstraintSet // Stack of constraint sets for nested expressions + contradictions []string + notContextStack []bool // Stack to track NOT contexts +} + +// DetectContradictions analyzes a query string and returns any contradictions found +func DetectContradictions(query string) ([]string, error) { + // Setup ANTLR parsing pipeline + input := antlr.NewInputStream(query) + lexer := grammar.NewFilterQueryLexer(input) + + // Error handling + lexerErrorListener := NewErrorListener() + lexer.RemoveErrorListeners() + lexer.AddErrorListener(lexerErrorListener) + + tokens := antlr.NewCommonTokenStream(lexer, 0) + parser := grammar.NewFilterQueryParser(tokens) + + parserErrorListener := NewErrorListener() + parser.RemoveErrorListeners() + parser.AddErrorListener(parserErrorListener) + + // Parse the query + tree := parser.Query() + + // Check for syntax errors + if len(parserErrorListener.SyntaxErrors) > 0 { + return nil, fmt.Errorf("syntax errors: %v", parserErrorListener.SyntaxErrors) + } + + // Create detector and visit tree + detector := &LogicalContradictionDetector{ + constraintStack: []*ConstraintSet{ + {Constraints: make(map[string][]FieldConstraint)}, + }, + contradictions: []string{}, + notContextStack: []bool{false}, + } + + detector.Visit(tree) + + // Deduplicate contradictions + seen := make(map[string]bool) + unique := []string{} + for _, c := range detector.contradictions { + if !seen[c] { + seen[c] = true + unique = append(unique, c) + } + } + + return unique, nil +} + +// Helper methods for constraint stack +func (d *LogicalContradictionDetector) currentConstraints() *ConstraintSet { + return d.constraintStack[len(d.constraintStack)-1] +} + +// Helper methods for NOT context +func (d *LogicalContradictionDetector) inNotContext() bool { + return d.notContextStack[len(d.notContextStack)-1] +} + +func (d *LogicalContradictionDetector) pushNotContext(value bool) { + d.notContextStack = append(d.notContextStack, value) +} + +func (d *LogicalContradictionDetector) popNotContext() { + if len(d.notContextStack) > 1 { + d.notContextStack = d.notContextStack[:len(d.notContextStack)-1] + } +} + +// Visit dispatches to the appropriate visit method +func (d *LogicalContradictionDetector) Visit(tree antlr.ParseTree) interface{} { + if tree == nil { + return nil + } + return tree.Accept(d) +} + +// VisitQuery is the entry point +func (d *LogicalContradictionDetector) VisitQuery(ctx *grammar.QueryContext) interface{} { + d.Visit(ctx.Expression()) + // Check final constraints + d.checkContradictions(d.currentConstraints()) + return nil +} + +// VisitExpression just passes through to OrExpression +func (d *LogicalContradictionDetector) VisitExpression(ctx *grammar.ExpressionContext) interface{} { + return d.Visit(ctx.OrExpression()) +} + +// VisitOrExpression handles OR logic +func (d *LogicalContradictionDetector) VisitOrExpression(ctx *grammar.OrExpressionContext) interface{} { + andExpressions := ctx.AllAndExpression() + + if len(andExpressions) == 1 { + // Single AND expression, just visit it + return d.Visit(andExpressions[0]) + } + + // Multiple AND expressions connected by OR + // Visit each branch to find contradictions within branches + for _, andExpr := range andExpressions { + // Save current constraints + savedConstraints := d.cloneConstraintSet(d.currentConstraints()) + + // Visit the AND expression + d.Visit(andExpr) + + // Restore constraints for next branch + d.constraintStack[len(d.constraintStack)-1] = savedConstraints + } + + return nil +} + +// VisitAndExpression handles AND logic (including implicit AND) +func (d *LogicalContradictionDetector) VisitAndExpression(ctx *grammar.AndExpressionContext) interface{} { + unaryExpressions := ctx.AllUnaryExpression() + + // Visit each unary expression, accumulating constraints + for _, unaryExpr := range unaryExpressions { + d.Visit(unaryExpr) + } + + return nil +} + +// VisitUnaryExpression handles NOT operator +func (d *LogicalContradictionDetector) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) interface{} { + hasNot := ctx.NOT() != nil + + if hasNot { + // Push new NOT context (toggle current value) + d.pushNotContext(!d.inNotContext()) + } + + result := d.Visit(ctx.Primary()) + + if hasNot { + // Pop NOT context + d.popNotContext() + } + + return result +} + +// VisitPrimary handles different primary expressions +func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext) interface{} { + if ctx.OrExpression() != nil { + // Parenthesized expression + // If we're in an AND context, we continue with the same constraint set + // Otherwise, we need to handle it specially + return d.Visit(ctx.OrExpression()) + } else if ctx.Comparison() != nil { + return d.Visit(ctx.Comparison()) + } else if ctx.FunctionCall() != nil { + // Handle function calls if needed + return nil + } else if ctx.FullText() != nil { + // Handle full text search if needed + return nil + } + + return nil +} + +// VisitComparison extracts constraints from comparisons +func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonContext) interface{} { + if ctx.Key() == nil { + return nil + } + + field := ctx.Key().GetText() + notContext := d.inNotContext() + + // Handle EXISTS + if ctx.EXISTS() != nil { + operator := qbtypes.FilterOperatorExists + if ctx.NOT() != nil { + operator = qbtypes.FilterOperatorNotExists + } + // Apply NOT context + if notContext { + operator = negateOperator(operator) + } + constraint := FieldConstraint{ + Field: field, + Operator: operator, + } + d.addConstraint(constraint) + return nil + } + + // Handle IN/NOT IN + if ctx.InClause() != nil { + values := d.extractValueList(ctx.InClause().(*grammar.InClauseContext).ValueList()) + operator := qbtypes.FilterOperatorIn + if notContext { + operator = negateOperator(operator) + } + constraint := FieldConstraint{ + Field: field, + Operator: operator, + Values: values, + } + d.addConstraint(constraint) + return nil + } + + if ctx.NotInClause() != nil { + values := d.extractValueList(ctx.NotInClause().(*grammar.NotInClauseContext).ValueList()) + operator := qbtypes.FilterOperatorNotIn + if notContext { + operator = negateOperator(operator) + } + constraint := FieldConstraint{ + Field: field, + Operator: operator, + Values: values, + } + d.addConstraint(constraint) + return nil + } + + // Handle BETWEEN + if ctx.BETWEEN() != nil { + values := ctx.AllValue() + if len(values) == 2 { + val1 := d.extractValue(values[0]) + val2 := d.extractValue(values[1]) + operator := qbtypes.FilterOperatorBetween + if ctx.NOT() != nil { + operator = qbtypes.FilterOperatorNotBetween + } + // Apply NOT context + if notContext { + operator = negateOperator(operator) + } + constraint := FieldConstraint{ + Field: field, + Operator: operator, + Values: []interface{}{val1, val2}, + } + d.addConstraint(constraint) + } + return nil + } + + // Handle regular comparisons + values := ctx.AllValue() + if len(values) > 0 { + value := d.extractValue(values[0]) + var operator qbtypes.FilterOperator + + if ctx.EQUALS() != nil { + operator = qbtypes.FilterOperatorEqual + } else if ctx.NOT_EQUALS() != nil || ctx.NEQ() != nil { + operator = qbtypes.FilterOperatorNotEqual + } else if ctx.LT() != nil { + operator = qbtypes.FilterOperatorLessThan + } else if ctx.LE() != nil { + operator = qbtypes.FilterOperatorLessThanOrEq + } else if ctx.GT() != nil { + operator = qbtypes.FilterOperatorGreaterThan + } else if ctx.GE() != nil { + operator = qbtypes.FilterOperatorGreaterThanOrEq + } else if ctx.LIKE() != nil { + operator = qbtypes.FilterOperatorLike + } else if ctx.ILIKE() != nil { + operator = qbtypes.FilterOperatorILike + } else if ctx.NOT_LIKE() != nil { + operator = qbtypes.FilterOperatorNotLike + } else if ctx.NOT_ILIKE() != nil { + operator = qbtypes.FilterOperatorNotILike + } else if ctx.REGEXP() != nil { + operator = qbtypes.FilterOperatorRegexp + if ctx.NOT() != nil { + operator = qbtypes.FilterOperatorNotRegexp + } + } else if ctx.CONTAINS() != nil { + operator = qbtypes.FilterOperatorContains + if ctx.NOT() != nil { + operator = qbtypes.FilterOperatorNotContains + } + } + + if operator != qbtypes.FilterOperatorUnknown { + // Apply NOT context if needed + if notContext { + operator = negateOperator(operator) + } + + constraint := FieldConstraint{ + Field: field, + Operator: operator, + Value: value, + } + d.addConstraint(constraint) + } + } + + // Check for contradictions after adding this constraint + d.checkContradictions(d.currentConstraints()) + + return nil +} + +// extractValue extracts the actual value from a ValueContext +func (d *LogicalContradictionDetector) extractValue(ctx grammar.IValueContext) interface{} { + if ctx.QUOTED_TEXT() != nil { + text := ctx.QUOTED_TEXT().GetText() + // Remove quotes + if len(text) >= 2 { + return text[1 : len(text)-1] + } + return text + } else if ctx.NUMBER() != nil { + return ctx.NUMBER().GetText() + } else if ctx.BOOL() != nil { + return ctx.BOOL().GetText() + } else if ctx.KEY() != nil { + return ctx.KEY().GetText() + } + return "" +} + +// extractValueList extracts values from a ValueListContext +func (d *LogicalContradictionDetector) extractValueList(ctx grammar.IValueListContext) []interface{} { + if ctx == nil { + return nil + } + + values := []interface{}{} + for _, val := range ctx.AllValue() { + values = append(values, d.extractValue(val)) + } + return values +} + +// addConstraint adds a constraint to the current set +func (d *LogicalContradictionDetector) addConstraint(constraint FieldConstraint) { + constraints := d.currentConstraints() + + // For positive operators that imply existence, add an implicit EXISTS constraint + // This mirrors the behavior of AddDefaultExistsFilter in the FilterOperator type + if constraint.Operator.AddDefaultExistsFilter() && !isNegativeOperator(constraint.Operator) { + // The field must exist for positive predicates + // This helps detect contradictions like: field = "value" AND field NOT EXISTS + existsConstraint := FieldConstraint{ + Field: constraint.Field, + Operator: qbtypes.FilterOperatorExists, + } + constraints.Constraints[constraint.Field] = append( + constraints.Constraints[constraint.Field], + existsConstraint, + ) + } + + constraints.Constraints[constraint.Field] = append( + constraints.Constraints[constraint.Field], + constraint, + ) +} + +// checkContradictions checks the given constraint set for contradictions +func (d *LogicalContradictionDetector) checkContradictions(constraintSet *ConstraintSet) { + for field, constraints := range constraintSet.Constraints { + if len(constraints) < 2 { + continue + } + + // Check for contradictions in this field's constraints + contradictions := d.findContradictionsInConstraints(field, constraints) + d.contradictions = append(d.contradictions, contradictions...) + } +} + +// findContradictionsInConstraints checks if a set of constraints on the same field contradict +func (d *LogicalContradictionDetector) findContradictionsInConstraints(field string, constraints []FieldConstraint) []string { + contradictions := []string{} + + // Group constraints by type for easier checking + var equalConstraints []FieldConstraint + var notEqualConstraints []FieldConstraint + var rangeConstraints []FieldConstraint + var inConstraints []FieldConstraint + var notInConstraints []FieldConstraint + var existsConstraints []FieldConstraint + var notExistsConstraints []FieldConstraint + var betweenConstraints []FieldConstraint + var notBetweenConstraints []FieldConstraint + var likeConstraints []FieldConstraint + + for _, c := range constraints { + switch c.Operator { + case qbtypes.FilterOperatorEqual: + equalConstraints = append(equalConstraints, c) + case qbtypes.FilterOperatorNotEqual: + notEqualConstraints = append(notEqualConstraints, c) + case qbtypes.FilterOperatorIn: + inConstraints = append(inConstraints, c) + case qbtypes.FilterOperatorNotIn: + notInConstraints = append(notInConstraints, c) + case qbtypes.FilterOperatorExists: + existsConstraints = append(existsConstraints, c) + case qbtypes.FilterOperatorNotExists: + notExistsConstraints = append(notExistsConstraints, c) + case qbtypes.FilterOperatorBetween: + betweenConstraints = append(betweenConstraints, c) + case qbtypes.FilterOperatorNotBetween: + notBetweenConstraints = append(notBetweenConstraints, c) + case qbtypes.FilterOperatorLike, qbtypes.FilterOperatorILike, + qbtypes.FilterOperatorNotLike, qbtypes.FilterOperatorNotILike: + likeConstraints = append(likeConstraints, c) + default: + // Handle range operators + if isRangeOperator(c.Operator) { + rangeConstraints = append(rangeConstraints, c) + } + } + } + + // Check for multiple different equality constraints + if len(equalConstraints) > 1 { + values := make(map[string]bool) + for _, c := range equalConstraints { + values[fmt.Sprintf("%v", c.Value)] = true + } + if len(values) > 1 { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' cannot equal multiple different values", field)) + } + } + + // Check equality vs not-equality + for _, eq := range equalConstraints { + for _, neq := range notEqualConstraints { + if fmt.Sprintf("%v", eq.Value) == fmt.Sprintf("%v", neq.Value) { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' cannot both equal and not equal '%v'", field, eq.Value)) + } + } + } + + // Check equality vs IN/NOT IN + for _, eq := range equalConstraints { + // Check against NOT IN + for _, notIn := range notInConstraints { + for _, v := range notIn.Values { + if fmt.Sprintf("%v", eq.Value) == fmt.Sprintf("%v", v) { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' equals '%v' but is in NOT IN list", field, eq.Value)) + } + } + } + + // Check against IN + for _, in := range inConstraints { + found := false + for _, v := range in.Values { + if fmt.Sprintf("%v", eq.Value) == fmt.Sprintf("%v", v) { + found = true + break + } + } + if !found { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' equals '%v' but is not in IN list", field, eq.Value)) + } + } + } + + // Check IN vs NOT IN overlap + for _, in := range inConstraints { + for _, notIn := range notInConstraints { + overlap := []string{} + for _, inVal := range in.Values { + for _, notInVal := range notIn.Values { + if fmt.Sprintf("%v", inVal) == fmt.Sprintf("%v", notInVal) { + overlap = append(overlap, fmt.Sprintf("%v", inVal)) + } + } + } + if len(overlap) > 0 { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' has overlapping IN and NOT IN values: %v", field, overlap)) + } + } + } + + // Check range contradictions + if len(rangeConstraints) > 0 { + if impossible := d.checkRangeContradictions(rangeConstraints); impossible { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' has contradictory range constraints", field)) + } + } + + // Check equality vs range + for _, eq := range equalConstraints { + if !d.valuesSatisfyRanges(eq.Value, rangeConstraints) { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' equals '%v' which violates range constraints", field, eq.Value)) + } + } + + // Check EXISTS contradictions + if len(existsConstraints) > 0 && len(notExistsConstraints) > 0 { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' cannot both exist and not exist", field)) + } + + // Check if NOT EXISTS contradicts with operators that imply existence + if len(notExistsConstraints) > 0 { + for _, c := range constraints { + if c.Operator.AddDefaultExistsFilter() && !isNegativeOperator(c.Operator) { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' has NOT EXISTS but also has %v which implies existence", + field, c.Operator)) + break + } + } + } + + // Check BETWEEN contradictions - need to check if ALL ranges have a common intersection + if len(betweenConstraints) >= 2 { + if !d.hasCommonIntersection(betweenConstraints) { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' has non-overlapping BETWEEN ranges", field)) + } + } + + // Check BETWEEN vs equality + for _, eq := range equalConstraints { + satisfiesAny := false + for _, between := range betweenConstraints { + if d.valueSatisfiesBetween(eq.Value, between) { + satisfiesAny = true + break + } + } + if len(betweenConstraints) > 0 && !satisfiesAny { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' equals '%v' which is outside BETWEEN range(s)", field, eq.Value)) + } + } + + // Check NOT BETWEEN vs equality + for _, eq := range equalConstraints { + for _, notBetween := range notBetweenConstraints { + if d.valueSatisfiesBetween(eq.Value, notBetween) { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' equals '%v' which is excluded by NOT BETWEEN range", field, eq.Value)) + } + } + } + + // Check if BETWEEN and NOT BETWEEN ranges make it impossible to have any value + if len(betweenConstraints) > 0 && len(notBetweenConstraints) > 0 { + // Check if the NOT BETWEEN completely covers the BETWEEN range + for _, between := range betweenConstraints { + if len(between.Values) == 2 { + bMin, err1 := parseNumericValue(between.Values[0]) + bMax, err2 := parseNumericValue(between.Values[1]) + if err1 == nil && err2 == nil { + // Check if this BETWEEN range has any values not excluded by NOT BETWEEN + hasValidValue := false + // Simple check: see if the endpoints or midpoint are valid + testValues := []float64{bMin, bMax, (bMin + bMax) / 2} + for _, testVal := range testValues { + valid := true + for _, notBetween := range notBetweenConstraints { + if d.valueSatisfiesBetween(testVal, notBetween) { + valid = false + break + } + } + if valid { + hasValidValue = true + break + } + } + if !hasValidValue { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' has BETWEEN and NOT BETWEEN ranges that exclude all values", field)) + } + } + } + } + } + + // Check LIKE pattern contradictions with exact values + for _, eq := range equalConstraints { + for _, like := range likeConstraints { + if like.Operator == qbtypes.FilterOperatorLike || like.Operator == qbtypes.FilterOperatorILike { + pattern := fmt.Sprintf("%v", like.Value) + value := fmt.Sprintf("%v", eq.Value) + if !d.matchesLikePattern(value, pattern) { + contradictions = append(contradictions, + fmt.Sprintf("Field '%s' equals '%v' which doesn't match LIKE pattern '%v'", + field, eq.Value, like.Value)) + } + } + } + } + + return contradictions +} + +// hasCommonIntersection checks if all BETWEEN ranges have a common intersection +func (d *LogicalContradictionDetector) hasCommonIntersection(betweens []FieldConstraint) bool { + if len(betweens) == 0 { + return true + } + + // Find the intersection of all ranges + var intersectionMin, intersectionMax float64 + initialized := false + + for _, b := range betweens { + if len(b.Values) != 2 { + continue + } + + min, err1 := parseNumericValue(b.Values[0]) + max, err2 := parseNumericValue(b.Values[1]) + if err1 != nil || err2 != nil { + continue // Skip non-numeric ranges + } + + if !initialized { + intersectionMin = min + intersectionMax = max + initialized = true + } else { + // Update intersection + if min > intersectionMin { + intersectionMin = min + } + if max < intersectionMax { + intersectionMax = max + } + } + } + + // If intersection is empty, ranges don't all overlap + return !initialized || intersectionMin <= intersectionMax +} + +// checkRangeContradictions checks if range constraints are satisfiable +func (d *LogicalContradictionDetector) checkRangeContradictions(constraints []FieldConstraint) bool { + // We need to find if there's any value that satisfies all constraints + + var lowerBounds []struct { + value float64 + inclusive bool + } + var upperBounds []struct { + value float64 + inclusive bool + } + + for _, c := range constraints { + val, err := parseNumericValue(c.Value) + if err != nil { + continue // Skip non-numeric values + } + + switch c.Operator { + case qbtypes.FilterOperatorGreaterThan: + lowerBounds = append(lowerBounds, struct { + value float64 + inclusive bool + }{val, false}) + case qbtypes.FilterOperatorGreaterThanOrEq: + lowerBounds = append(lowerBounds, struct { + value float64 + inclusive bool + }{val, true}) + case qbtypes.FilterOperatorLessThan: + upperBounds = append(upperBounds, struct { + value float64 + inclusive bool + }{val, false}) + case qbtypes.FilterOperatorLessThanOrEq: + upperBounds = append(upperBounds, struct { + value float64 + inclusive bool + }{val, true}) + } + } + + // Find the most restrictive lower bound + var effectiveLower *float64 + lowerInclusive := false + for _, lb := range lowerBounds { + if effectiveLower == nil || lb.value > *effectiveLower || + (lb.value == *effectiveLower && !lb.inclusive && lowerInclusive) { + effectiveLower = &lb.value + lowerInclusive = lb.inclusive + } + } + + // Find the most restrictive upper bound + var effectiveUpper *float64 + upperInclusive := false + for _, ub := range upperBounds { + if effectiveUpper == nil || ub.value < *effectiveUpper || + (ub.value == *effectiveUpper && !ub.inclusive && upperInclusive) { + effectiveUpper = &ub.value + upperInclusive = ub.inclusive + } + } + + // Check if we have both bounds and they're contradictory + if effectiveLower != nil && effectiveUpper != nil { + if *effectiveLower > *effectiveUpper { + return true + } + if *effectiveLower == *effectiveUpper && (!lowerInclusive || !upperInclusive) { + return true + } + } + + return false +} + +// valuesSatisfyRanges checks if a value satisfies all range constraints +func (d *LogicalContradictionDetector) valuesSatisfyRanges(value interface{}, constraints []FieldConstraint) bool { + val, err := parseNumericValue(value) + if err != nil { + return true // If not numeric, we can't check + } + + for _, c := range constraints { + cVal, err := parseNumericValue(c.Value) + if err != nil { + continue + } + + switch c.Operator { + case qbtypes.FilterOperatorGreaterThan: + if val <= cVal { + return false + } + case qbtypes.FilterOperatorGreaterThanOrEq: + if val < cVal { + return false + } + case qbtypes.FilterOperatorLessThan: + if val >= cVal { + return false + } + case qbtypes.FilterOperatorLessThanOrEq: + if val > cVal { + return false + } + } + } + + return true +} + +// valueSatisfiesBetween checks if a value is within a BETWEEN range +func (d *LogicalContradictionDetector) valueSatisfiesBetween(value interface{}, between FieldConstraint) bool { + if len(between.Values) != 2 { + return false + } + + val, err := parseNumericValue(value) + if err != nil { + return true // Can't check non-numeric + } + + min, err1 := parseNumericValue(between.Values[0]) + max, err2 := parseNumericValue(between.Values[1]) + + if err1 != nil || err2 != nil { + return true + } + + return val >= min && val <= max +} + +// matchesLikePattern is a simple pattern matcher for LIKE +func (d *LogicalContradictionDetector) matchesLikePattern(value, pattern string) bool { + // Simple implementation - just check prefix/suffix with % + if strings.HasPrefix(pattern, "%") && strings.HasSuffix(pattern, "%") { + return strings.Contains(value, pattern[1:len(pattern)-1]) + } else if strings.HasPrefix(pattern, "%") { + return strings.HasSuffix(value, pattern[1:]) + } else if strings.HasSuffix(pattern, "%") { + return strings.HasPrefix(value, pattern[:len(pattern)-1]) + } + return value == pattern +} + +// cloneConstraintSet creates a deep copy of a constraint set +func (d *LogicalContradictionDetector) cloneConstraintSet(set *ConstraintSet) *ConstraintSet { + newSet := &ConstraintSet{ + Constraints: make(map[string][]FieldConstraint), + } + + for field, constraints := range set.Constraints { + newConstraints := make([]FieldConstraint, len(constraints)) + copy(newConstraints, constraints) + newSet.Constraints[field] = newConstraints + } + + return newSet +} + +// parseNumericValue attempts to parse a value as a number +func parseNumericValue(value interface{}) (float64, error) { + switch v := value.(type) { + case float64: + return v, nil + case int: + return float64(v), nil + case string: + return strconv.ParseFloat(v, 64) + default: + return 0, fmt.Errorf("not a numeric value") + } +} + +// negateOperator returns the negated version of an operator +func negateOperator(op qbtypes.FilterOperator) qbtypes.FilterOperator { + switch op { + case qbtypes.FilterOperatorEqual: + return qbtypes.FilterOperatorNotEqual + case qbtypes.FilterOperatorNotEqual: + return qbtypes.FilterOperatorEqual + case qbtypes.FilterOperatorLessThan: + return qbtypes.FilterOperatorGreaterThanOrEq + case qbtypes.FilterOperatorLessThanOrEq: + return qbtypes.FilterOperatorGreaterThan + case qbtypes.FilterOperatorGreaterThan: + return qbtypes.FilterOperatorLessThanOrEq + case qbtypes.FilterOperatorGreaterThanOrEq: + return qbtypes.FilterOperatorLessThan + case qbtypes.FilterOperatorIn: + return qbtypes.FilterOperatorNotIn + case qbtypes.FilterOperatorNotIn: + return qbtypes.FilterOperatorIn + case qbtypes.FilterOperatorExists: + return qbtypes.FilterOperatorNotExists + case qbtypes.FilterOperatorNotExists: + return qbtypes.FilterOperatorExists + case qbtypes.FilterOperatorLike: + return qbtypes.FilterOperatorNotLike + case qbtypes.FilterOperatorNotLike: + return qbtypes.FilterOperatorLike + case qbtypes.FilterOperatorILike: + return qbtypes.FilterOperatorNotILike + case qbtypes.FilterOperatorNotILike: + return qbtypes.FilterOperatorILike + case qbtypes.FilterOperatorBetween: + return qbtypes.FilterOperatorNotBetween + case qbtypes.FilterOperatorNotBetween: + return qbtypes.FilterOperatorBetween + case qbtypes.FilterOperatorRegexp: + return qbtypes.FilterOperatorNotRegexp + case qbtypes.FilterOperatorNotRegexp: + return qbtypes.FilterOperatorRegexp + case qbtypes.FilterOperatorContains: + return qbtypes.FilterOperatorNotContains + case qbtypes.FilterOperatorNotContains: + return qbtypes.FilterOperatorContains + default: + return op + } +} + +// isRangeOperator returns true if the operator is a range comparison operator +func isRangeOperator(op qbtypes.FilterOperator) bool { + switch op { + case qbtypes.FilterOperatorLessThan, + qbtypes.FilterOperatorLessThanOrEq, + qbtypes.FilterOperatorGreaterThan, + qbtypes.FilterOperatorGreaterThanOrEq: + return true + default: + return false + } +} + +// isNegativeOperator returns true if the operator is a negative/exclusion operator +func isNegativeOperator(op qbtypes.FilterOperator) bool { + switch op { + case qbtypes.FilterOperatorNotEqual, + qbtypes.FilterOperatorNotIn, + qbtypes.FilterOperatorNotExists, + qbtypes.FilterOperatorNotLike, + qbtypes.FilterOperatorNotILike, + qbtypes.FilterOperatorNotBetween, + qbtypes.FilterOperatorNotRegexp, + qbtypes.FilterOperatorNotContains: + return true + default: + return false + } +} diff --git a/pkg/querybuilder/never_true_test.go b/pkg/querybuilder/never_true_test.go new file mode 100644 index 000000000000..cfb101512ddb --- /dev/null +++ b/pkg/querybuilder/never_true_test.go @@ -0,0 +1,395 @@ +package querybuilder + +import ( + "strings" + "testing" +) + +func TestContradictionDetection(t *testing.T) { + tests := []struct { + name string + query string + hasContradiction bool + expectedErrors []string + }{ + { + name: "Simple equality contradiction", + query: `service.name = 'redis' service.name='route' http.status_code=200`, + hasContradiction: true, + expectedErrors: []string{"service.name"}, + }, + { + name: "Equal and not equal same value", + query: `service.name = 'redis' AND service.name != 'redis'`, + hasContradiction: true, + expectedErrors: []string{"service.name"}, + }, + { + name: "Range contradiction", + query: `http.status_code > 500 AND http.status_code < 400`, + hasContradiction: true, + expectedErrors: []string{"http.status_code"}, + }, + { + name: "IN and NOT IN overlap", + query: `service.name IN ('redis', 'mysql') AND service.name NOT IN ('redis', 'postgres')`, + hasContradiction: true, + expectedErrors: []string{"service.name"}, + }, + { + name: "EXISTS and NOT EXISTS", + query: `custom.tag EXISTS AND custom.tag NOT EXISTS`, + hasContradiction: true, + expectedErrors: []string{"custom.tag"}, + }, + { + name: "Equal and NOT IN containing value", + query: `service.name = 'redis' AND service.name NOT IN ('redis', 'mysql')`, + hasContradiction: true, + expectedErrors: []string{"service.name"}, + }, + { + name: "Non-overlapping BETWEEN ranges", + query: `http.status_code BETWEEN 200 AND 299 AND http.status_code BETWEEN 400 AND 499`, + hasContradiction: true, + expectedErrors: []string{"http.status_code"}, + }, + { + name: "Valid query with no contradictions", + query: `service.name = 'redis' AND http.status_code >= 200 AND http.status_code < 300`, + hasContradiction: false, + expectedErrors: []string{}, + }, + { + name: "OR expression - no contradiction", + query: `service.name = 'redis' OR service.name = 'mysql'`, + hasContradiction: false, + expectedErrors: []string{}, + }, + { + name: "Complex valid query", + query: `(service.name = 'redis' OR service.name = 'mysql') AND http.status_code = 200`, + hasContradiction: false, + expectedErrors: []string{}, + }, + { + name: "Negated contradiction", + query: `NOT (service.name = 'redis') AND service.name = 'redis'`, + hasContradiction: true, + expectedErrors: []string{"service.name"}, + }, + { + name: "Multiple field contradictions", + query: `service.name = 'redis' AND service.name = 'mysql' AND http.status_code = 200 AND http.status_code = 404`, + hasContradiction: true, + expectedErrors: []string{"service.name", "http.status_code"}, + }, + { + name: "Implicit AND with contradiction", + query: `service.name='redis' service.name='mysql'`, + hasContradiction: true, + expectedErrors: []string{"service.name"}, + }, + { + name: "Equal with incompatible range", + query: `http.status_code = 200 AND http.status_code > 300`, + hasContradiction: true, + expectedErrors: []string{"http.status_code"}, + }, + { + name: "Complex nested contradiction", + query: `(service.name = 'redis' AND http.status_code = 200) AND (service.name = 'mysql' AND http.status_code = 200)`, + hasContradiction: true, + expectedErrors: []string{"service.name"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + contradictions, err := DetectContradictions(tt.query) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + hasContradiction := len(contradictions) > 0 + + if hasContradiction != tt.hasContradiction { + t.Errorf("expected hasContradiction=%v, got %v. Contradictions: %v", + tt.hasContradiction, hasContradiction, contradictions) + } + + if tt.hasContradiction { + // Check that we found contradictions for expected fields + for _, expectedField := range tt.expectedErrors { + found := false + for _, contradiction := range contradictions { + if strings.Contains(contradiction, expectedField) { + found = true + break + } + } + if !found { + t.Errorf("expected contradiction for field %s, but not found. Got: %v", + expectedField, contradictions) + } + } + } + }) + } +} + +func TestComplexNestedContradictions(t *testing.T) { + tests := []struct { + name string + query string + hasContradiction bool + expectedFields []string + description string + }{ + // Complex nested AND/OR combinations + { + name: "Nested AND with contradiction in inner expression", + query: `(service.name = 'redis' AND http.status_code = 200) AND (service.name = 'mysql' AND http.status_code = 200)`, + hasContradiction: true, + expectedFields: []string{"service.name"}, + description: "Inner ANDs both valid, but combined they contradict on service.name", + }, + { + name: "OR with contradictory AND branches - no contradiction", + query: `(service.name = 'redis' AND service.name = 'mysql') OR (http.status_code = 200)`, + hasContradiction: true, + expectedFields: []string{"service.name"}, + description: "First branch impossible", + }, + { + name: "Deeply nested contradiction", + query: `((service.name = 'redis' AND (http.status_code > 200 AND http.status_code < 200)) AND region = 'us-east')`, + hasContradiction: true, + expectedFields: []string{"http.status_code"}, + description: "Nested impossible range condition", + }, + { + name: "Multiple field contradictions in nested structure", + query: `(service.name = 'redis' AND service.name != 'redis') AND (http.status_code = 200 AND http.status_code = 404)`, + hasContradiction: true, + expectedFields: []string{"service.name", "http.status_code"}, + description: "Both nested expressions have contradictions", + }, + + // Complex BETWEEN contradictions + { + name: "BETWEEN with overlapping ranges - valid", + query: `http.status_code BETWEEN 200 AND 299 AND http.status_code BETWEEN 250 AND 350`, + hasContradiction: false, + expectedFields: []string{}, + description: "Ranges overlap at 250-299, so valid", + }, + { + name: "BETWEEN with exact value outside range", + query: `http.status_code = 500 AND http.status_code BETWEEN 200 AND 299`, + hasContradiction: true, + expectedFields: []string{"http.status_code"}, + description: "Exact value outside BETWEEN range", + }, + { + name: "Multiple BETWEEN with no overlap", + query: `(latency BETWEEN 100 AND 200) AND (latency BETWEEN 300 AND 400) AND (latency BETWEEN 500 AND 600)`, + hasContradiction: true, + expectedFields: []string{"latency"}, + description: "Three non-overlapping ranges", + }, + + // Complex IN/NOT IN combinations + { + name: "IN with nested NOT IN contradiction", + query: `service.name IN ('redis', 'mysql', 'postgres') AND (service.name NOT IN ('mysql', 'postgres') AND service.name NOT IN ('redis'))`, + hasContradiction: true, + expectedFields: []string{"service.name"}, + description: "Combined NOT IN excludes all values from IN", + }, + { + name: "Complex valid IN/NOT IN", + query: `service.name IN ('redis', 'mysql', 'postgres') AND service.name NOT IN ('mongodb', 'cassandra')`, + hasContradiction: false, + expectedFields: []string{}, + description: "Non-overlapping IN and NOT IN lists", + }, + + // Implicit AND with complex expressions + { + name: "Implicit AND with nested contradiction", + query: `service.name='redis' (http.status_code > 500 http.status_code < 400)`, + hasContradiction: true, + expectedFields: []string{"http.status_code"}, + description: "Implicit AND creates impossible range", + }, + { + name: "Mixed implicit and explicit AND", + query: `service.name='redis' service.name='mysql' AND http.status_code=200`, + hasContradiction: true, + expectedFields: []string{"service.name"}, + description: "Implicit AND between service names creates contradiction", + }, + + // NOT operator complexities + { + name: "Double negation with contradiction", + query: `NOT (NOT (service.name = 'redis')) AND service.name = 'mysql'`, + hasContradiction: true, + expectedFields: []string{"service.name"}, + description: "Double NOT cancels out, creating contradiction", + }, + + // Range conditions with multiple operators + { + name: "Chained range conditions creating impossible range", + query: `value > 100 AND value < 200 AND value > 300 AND value < 400`, + hasContradiction: true, + expectedFields: []string{"value"}, + description: "Multiple ranges that cannot be satisfied simultaneously", + }, + { + name: "Valid narrowing range", + query: `value > 100 AND value < 400 AND value > 200 AND value < 300`, + hasContradiction: false, + expectedFields: []string{}, + description: "Ranges narrow down to valid 200-300 range", + }, + + // Mixed operator types + { + name: "LIKE pattern with exact value contradiction", + query: `service.name = 'redis-cache-01' AND service.name LIKE 'mysql%'`, + hasContradiction: true, + expectedFields: []string{"service.name"}, + description: "Exact value doesn't match LIKE pattern", + }, + { + name: "EXISTS with value contradiction", + query: `custom.tag EXISTS AND custom.tag = 'value' AND custom.tag NOT EXISTS`, + hasContradiction: true, + expectedFields: []string{"custom.tag"}, + description: "Field both exists with value and doesn't exist", + }, + + // Edge cases + { + name: "Same field different types", + query: `http.status_code = '200' AND http.status_code = 200`, + hasContradiction: false, // Depends on type coercion + expectedFields: []string{}, + description: "Same value different types - implementation dependent", + }, + { + name: "Complex parentheses with valid expression", + query: `((((service.name = 'redis')))) AND ((((http.status_code = 200))))`, + hasContradiction: false, + expectedFields: []string{}, + description: "Multiple parentheses levels but valid expression", + }, + + // Real-world complex scenarios + { + name: "Monitoring query with impossible conditions", + query: `service.name = 'api-gateway' AND + http.status_code >= 500 AND + http.status_code < 500 AND + region IN ('us-east-1', 'us-west-2') AND + region NOT IN ('us-east-1', 'us-west-2', 'eu-west-1')`, + hasContradiction: true, + expectedFields: []string{"http.status_code", "region"}, + description: "Multiple contradictions in monitoring query", + }, + { + name: "Valid complex monitoring query", + query: `(service.name = 'api-gateway' OR service.name = 'web-server') AND + http.status_code >= 400 AND + http.status_code < 500 AND + region IN ('us-east-1', 'us-west-2') AND + latency > 1000`, + hasContradiction: false, + expectedFields: []string{}, + description: "Complex but valid monitoring conditions", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + contradictions, err := DetectContradictions(tt.query) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + hasContradiction := len(contradictions) > 0 + + if hasContradiction != tt.hasContradiction { + t.Errorf("Test: %s\nDescription: %s\nExpected hasContradiction=%v, got %v\nContradictions: %v", + tt.name, tt.description, tt.hasContradiction, hasContradiction, contradictions) + } + + if tt.hasContradiction { + // Check that we found contradictions for expected fields + for _, expectedField := range tt.expectedFields { + found := false + for _, contradiction := range contradictions { + if strings.Contains(contradiction, expectedField) { + found = true + break + } + } + if !found { + t.Errorf("Test: %s\nExpected contradiction for field %s, but not found.\nGot: %v", + tt.name, expectedField, contradictions) + } + } + } + }) + } +} + +func TestExpressionLevelHandling(t *testing.T) { + tests := []struct { + name string + query string + hasContradiction bool + description string + }{ + { + name: "OR at top level - no contradiction", + query: `service.name = 'redis' OR service.name = 'mysql'`, + hasContradiction: false, + description: "Top level OR should not check for contradictions", + }, + { + name: "AND within OR - contradiction only in AND branch", + query: `(service.name = 'redis' AND service.name = 'mysql') OR http.status_code = 200`, + hasContradiction: true, + description: "Contradiction in one OR branch doesn't make whole expression contradictory", + }, + { + name: "Nested OR within AND - valid", + query: `http.status_code = 200 AND (service.name = 'redis' OR service.name = 'mysql')`, + hasContradiction: false, + description: "OR within AND is valid", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + contradictions, err := DetectContradictions(tt.query) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + hasContradiction := len(contradictions) > 0 + + if hasContradiction != tt.hasContradiction { + t.Errorf("Test: %s\nDescription: %s\nExpected hasContradiction=%v, got %v\nContradictions: %v", + tt.name, tt.description, tt.hasContradiction, hasContradiction, contradictions) + } + }) + } +}