mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
cloud renamed its composition verb to zip own word, and this module publishes the other half of that seam.
941 lines
27 KiB
Go
941 lines
27 KiB
Go
package querybuilder
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/antlr4-go/antlr/v4"
|
|
"github.com/hanzoai/o11y/pkg/errors"
|
|
grammar "github.com/hanzoai/o11y/pkg/parser/filterquery/grammar"
|
|
qbtypes "github.com/hanzoai/o11y/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
)
|
|
|
|
// 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, errors.NewInvalidInputf(errors.CodeInvalidInput, "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
|
|
if ctx.NOT() != nil {
|
|
operator = qbtypes.FilterOperatorNotLike
|
|
}
|
|
} else if ctx.ILIKE() != nil {
|
|
operator = qbtypes.FilterOperatorILike
|
|
if ctx.NOT() != 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, errors.NewInvalidInputf(errors.CodeInvalidInput, "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
|
|
}
|
|
}
|