fix: exception on resource filters with numeric values (#9028)

This commit is contained in:
Nityananda Gohain 2025-09-14 18:30:16 +05:30 committed by GitHub
parent ae58915020
commit a686941880
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 81 additions and 33 deletions

View File

@ -22,21 +22,20 @@ func NewConditionBuilder(fm qbtypes.FieldMapper) *defaultConditionBuilder {
func valueForIndexFilter(op qbtypes.FilterOperator, key *telemetrytypes.TelemetryFieldKey, value any) any { func valueForIndexFilter(op qbtypes.FilterOperator, key *telemetrytypes.TelemetryFieldKey, value any) any {
switch v := value.(type) { switch v := value.(type) {
case string:
if op == qbtypes.FilterOperatorEqual || op == qbtypes.FilterOperatorNotEqual {
return fmt.Sprintf(`%%%s":"%s%%`, key.Name, v)
}
return fmt.Sprintf(`%%%s%%%s%%`, key.Name, v)
case []any: case []any:
// assuming array will always be for in and not in // assuming array will always be for in and not in
values := make([]string, 0, len(v)) values := make([]string, 0, len(v))
for _, v := range v { for _, v := range v {
values = append(values, fmt.Sprintf(`%%%s":"%s%%`, key.Name, v)) values = append(values, fmt.Sprintf(`%%%s":"%s%%`, key.Name, querybuilder.FormatValueForContains(v)))
} }
return values return values
default:
// format to string for anything else as we store resource values as string
if op == qbtypes.FilterOperatorEqual || op == qbtypes.FilterOperatorNotEqual {
return fmt.Sprintf(`%%%s":"%s%%`, key.Name, querybuilder.FormatValueForContains(v))
}
return fmt.Sprintf(`%%%s%%%s%%`, key.Name, querybuilder.FormatValueForContains(v))
} }
// resource table expects string value
return fmt.Sprintf(`%%%v%%`, value)
} }
func keyIndexFilter(key *telemetrytypes.TelemetryFieldKey) any { func keyIndexFilter(key *telemetrytypes.TelemetryFieldKey) any {
@ -55,15 +54,9 @@ func (b *defaultConditionBuilder) ConditionFor(
return "true", nil return "true", nil
} }
switch op { // except for in, not in, between, not between all other operators should have formatted value
case qbtypes.FilterOperatorContains, // as we store resource values as string
qbtypes.FilterOperatorNotContains, formattedValue := querybuilder.FormatValueForContains(value)
qbtypes.FilterOperatorILike,
qbtypes.FilterOperatorNotILike,
qbtypes.FilterOperatorLike,
qbtypes.FilterOperatorNotLike:
value = querybuilder.FormatValueForContains(value)
}
column, err := b.fm.ColumnFor(ctx, key) column, err := b.fm.ColumnFor(ctx, key)
if err != nil { if err != nil {
@ -81,34 +74,34 @@ func (b *defaultConditionBuilder) ConditionFor(
switch op { switch op {
case qbtypes.FilterOperatorEqual: case qbtypes.FilterOperatorEqual:
return sb.And( return sb.And(
sb.E(fieldName, value), sb.E(fieldName, formattedValue),
keyIdxFilter, keyIdxFilter,
sb.Like(column.Name, valueForIndexFilter), sb.Like(column.Name, valueForIndexFilter),
), nil ), nil
case qbtypes.FilterOperatorNotEqual: case qbtypes.FilterOperatorNotEqual:
return sb.And( return sb.And(
sb.NE(fieldName, value), sb.NE(fieldName, formattedValue),
sb.NotLike(column.Name, valueForIndexFilter), sb.NotLike(column.Name, valueForIndexFilter),
), nil ), nil
case qbtypes.FilterOperatorGreaterThan: case qbtypes.FilterOperatorGreaterThan:
return sb.And(sb.GT(fieldName, value), keyIdxFilter), nil return sb.And(sb.GT(fieldName, formattedValue), keyIdxFilter), nil
case qbtypes.FilterOperatorGreaterThanOrEq: case qbtypes.FilterOperatorGreaterThanOrEq:
return sb.And(sb.GE(fieldName, value), keyIdxFilter), nil return sb.And(sb.GE(fieldName, formattedValue), keyIdxFilter), nil
case qbtypes.FilterOperatorLessThan: case qbtypes.FilterOperatorLessThan:
return sb.And(sb.LT(fieldName, value), keyIdxFilter), nil return sb.And(sb.LT(fieldName, formattedValue), keyIdxFilter), nil
case qbtypes.FilterOperatorLessThanOrEq: case qbtypes.FilterOperatorLessThanOrEq:
return sb.And(sb.LE(fieldName, value), keyIdxFilter), nil return sb.And(sb.LE(fieldName, formattedValue), keyIdxFilter), nil
case qbtypes.FilterOperatorLike, qbtypes.FilterOperatorILike: case qbtypes.FilterOperatorLike, qbtypes.FilterOperatorILike:
return sb.And( return sb.And(
sb.ILike(fieldName, value), sb.ILike(fieldName, formattedValue),
keyIdxFilter, keyIdxFilter,
sb.ILike(column.Name, valueForIndexFilter), sb.ILike(column.Name, valueForIndexFilter),
), nil ), nil
case qbtypes.FilterOperatorNotLike, qbtypes.FilterOperatorNotILike: case qbtypes.FilterOperatorNotLike, qbtypes.FilterOperatorNotILike:
// no index filter: as cannot apply `not contains x%y` as y can be somewhere else // no index filter: as cannot apply `not contains x%y` as y can be somewhere else
return sb.And( return sb.And(
sb.NotILike(fieldName, value), sb.NotILike(fieldName, formattedValue),
), nil ), nil
case qbtypes.FilterOperatorBetween: case qbtypes.FilterOperatorBetween:
@ -119,7 +112,7 @@ func (b *defaultConditionBuilder) ConditionFor(
if len(values) != 2 { if len(values) != 2 {
return "", qbtypes.ErrBetweenValues return "", qbtypes.ErrBetweenValues
} }
return sb.And(keyIdxFilter, sb.Between(fieldName, values[0], values[1])), nil return sb.And(keyIdxFilter, sb.Between(fieldName, querybuilder.FormatValueForContains(values[0]), querybuilder.FormatValueForContains(values[1]))), nil
case qbtypes.FilterOperatorNotBetween: case qbtypes.FilterOperatorNotBetween:
values, ok := value.([]any) values, ok := value.([]any)
if !ok { if !ok {
@ -128,7 +121,7 @@ func (b *defaultConditionBuilder) ConditionFor(
if len(values) != 2 { if len(values) != 2 {
return "", qbtypes.ErrBetweenValues return "", qbtypes.ErrBetweenValues
} }
return sb.And(sb.NotBetween(fieldName, values[0], values[1])), nil return sb.And(sb.NotBetween(fieldName, querybuilder.FormatValueForContains(values[0]), querybuilder.FormatValueForContains(values[1]))), nil
case qbtypes.FilterOperatorIn: case qbtypes.FilterOperatorIn:
values, ok := value.([]any) values, ok := value.([]any)
@ -137,7 +130,7 @@ func (b *defaultConditionBuilder) ConditionFor(
} }
inConditions := make([]string, 0, len(values)) inConditions := make([]string, 0, len(values))
for _, v := range values { for _, v := range values {
inConditions = append(inConditions, sb.E(fieldName, v)) inConditions = append(inConditions, sb.E(fieldName, querybuilder.FormatValueForContains(v)))
} }
mainCondition := sb.Or(inConditions...) mainCondition := sb.Or(inConditions...)
valConditions := make([]string, 0, len(values)) valConditions := make([]string, 0, len(values))
@ -156,7 +149,7 @@ func (b *defaultConditionBuilder) ConditionFor(
} }
notInConditions := make([]string, 0, len(values)) notInConditions := make([]string, 0, len(values))
for _, v := range values { for _, v := range values {
notInConditions = append(notInConditions, sb.NE(fieldName, v)) notInConditions = append(notInConditions, sb.NE(fieldName, querybuilder.FormatValueForContains(v)))
} }
mainCondition := sb.And(notInConditions...) mainCondition := sb.And(notInConditions...)
valConditions := make([]string, 0, len(values)) valConditions := make([]string, 0, len(values))
@ -180,24 +173,24 @@ func (b *defaultConditionBuilder) ConditionFor(
case qbtypes.FilterOperatorRegexp: case qbtypes.FilterOperatorRegexp:
return sb.And( return sb.And(
fmt.Sprintf("match(%s, %s)", fieldName, sb.Var(value)), fmt.Sprintf("match(%s, %s)", fieldName, sb.Var(formattedValue)),
keyIdxFilter, keyIdxFilter,
), nil ), nil
case qbtypes.FilterOperatorNotRegexp: case qbtypes.FilterOperatorNotRegexp:
return sb.And( return sb.And(
fmt.Sprintf("NOT match(%s, %s)", fieldName, sb.Var(value)), fmt.Sprintf("NOT match(%s, %s)", fieldName, sb.Var(formattedValue)),
), nil ), nil
case qbtypes.FilterOperatorContains: case qbtypes.FilterOperatorContains:
return sb.And( return sb.And(
sb.ILike(fieldName, fmt.Sprintf(`%%%s%%`, value)), sb.ILike(fieldName, fmt.Sprintf(`%%%s%%`, formattedValue)),
keyIdxFilter, keyIdxFilter,
sb.ILike(column.Name, valueForIndexFilter), sb.ILike(column.Name, valueForIndexFilter),
), nil ), nil
case qbtypes.FilterOperatorNotContains: case qbtypes.FilterOperatorNotContains:
// no index filter: as cannot apply `not contains x%y` as y can be somewhere else // no index filter: as cannot apply `not contains x%y` as y can be somewhere else
return sb.And( return sb.And(
sb.NotILike(fieldName, fmt.Sprintf(`%%%s%%`, value)), sb.NotILike(fieldName, fmt.Sprintf(`%%%s%%`, formattedValue)),
), nil ), nil
} }
return "", qbtypes.ErrUnsupportedOperator return "", qbtypes.ErrUnsupportedOperator

View File

@ -143,6 +143,61 @@ func TestConditionBuilder(t *testing.T) {
expected: "simpleJSONHas(labels, 'k8s.namespace.name') <> ?", expected: "simpleJSONHas(labels, 'k8s.namespace.name') <> ?",
expectedArgs: []any{true}, expectedArgs: []any{true},
}, },
{
name: "number_equals",
key: &telemetrytypes.TelemetryFieldKey{
Name: "test_num",
FieldContext: telemetrytypes.FieldContextResource,
},
op: querybuildertypesv5.FilterOperatorEqual,
value: 1,
expected: "simpleJSONExtractString(labels, 'test_num') = ? AND labels LIKE ? AND labels LIKE ?",
expectedArgs: []any{"1", "%test_num%", "%test_num\":\"1%"},
},
{
name: "number_gt",
key: &telemetrytypes.TelemetryFieldKey{
Name: "test_num",
FieldContext: telemetrytypes.FieldContextResource,
},
op: querybuildertypesv5.FilterOperatorGreaterThan,
value: 1,
expected: "simpleJSONExtractString(labels, 'test_num') > ? AND labels LIKE ?",
expectedArgs: []any{"1", "%test_num%"},
},
{
name: "number_in",
key: &telemetrytypes.TelemetryFieldKey{
Name: "test_num",
FieldContext: telemetrytypes.FieldContextResource,
},
op: querybuildertypesv5.FilterOperatorIn,
value: []any{1, 2},
expected: "(simpleJSONExtractString(labels, 'test_num') = ? OR simpleJSONExtractString(labels, 'test_num') = ?) AND labels LIKE ? AND (labels LIKE ? OR labels LIKE ?)",
expectedArgs: []any{"1", "2", "%test_num%", "%test_num\":\"1%", "%test_num\":\"2%"},
},
{
name: "number_between",
key: &telemetrytypes.TelemetryFieldKey{
Name: "test_num",
FieldContext: telemetrytypes.FieldContextResource,
},
op: querybuildertypesv5.FilterOperatorBetween,
value: []any{1, 2},
expected: "labels LIKE ? AND simpleJSONExtractString(labels, 'test_num') BETWEEN ? AND ?",
expectedArgs: []any{"%test_num%", "1", "2"},
},
{
name: "string_regexp",
key: &telemetrytypes.TelemetryFieldKey{
Name: "k8s.namespace.name",
FieldContext: telemetrytypes.FieldContextResource,
},
op: querybuildertypesv5.FilterOperatorRegexp,
value: "ban.*",
expected: "match(simpleJSONExtractString(labels, 'k8s.namespace.name'), ?) AND labels LIKE ?",
expectedArgs: []any{"ban.*", "%k8s.namespace.name%"},
},
} }
fm := NewFieldMapper() fm := NewFieldMapper()