feat: warn when LIKE/ILIKE is used without any %/_ (#9098)

* feat: warn when LIKE/ILIKE is used without any %/_

Signed-off-by: “niladrix719” <niladrix719@gmail.com>
This commit is contained in:
Niladri Adhikary 2025-09-16 11:53:26 +05:30 committed by GitHub
parent 2acdd101d8
commit c5051128fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 108 additions and 0 deletions

View File

@ -18,6 +18,8 @@ import (
var searchTroubleshootingGuideURL = "https://signoz.io/docs/userguide/search-troubleshooting/"
const stringMatchingOperatorDocURL = "https://signoz.io/docs/userguide/operators-reference/#string-matching-operators"
// filterExpressionVisitor implements the FilterQueryVisitor interface
// to convert the parsed filter expressions into ClickHouse WHERE clause
type filterExpressionVisitor struct {
@ -533,11 +535,13 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
if ctx.NOT() != nil {
op = qbtypes.FilterOperatorNotLike
}
v.warnIfLikeWithoutWildcards("LIKE", value)
} else if ctx.ILIKE() != nil {
op = qbtypes.FilterOperatorILike
if ctx.NOT() != nil {
op = qbtypes.FilterOperatorNotILike
}
v.warnIfLikeWithoutWildcards("ILIKE", value)
} else if ctx.REGEXP() != nil {
op = qbtypes.FilterOperatorRegexp
if ctx.NOT() != nil {
@ -571,6 +575,19 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
return "" // Should not happen with valid input
}
// warnIfLikeWithoutWildcards adds a guidance warning when LIKE/ILIKE is used without wildcards
func (v *filterExpressionVisitor) warnIfLikeWithoutWildcards(op string, value any) {
if hasLikeWildcards(value) {
return
}
msg := op + " operator used without wildcards (% or _). Consider using = operator for exact matches or add wildcards for pattern matching."
v.warnings = append(v.warnings, msg)
if v.mainWarnURL == "" {
v.mainWarnURL = stringMatchingOperatorDocURL
}
}
// VisitInClause handles IN expressions
func (v *filterExpressionVisitor) VisitInClause(ctx *grammar.InClauseContext) any {
if ctx.ValueList() != nil {
@ -871,6 +888,15 @@ func (v *filterExpressionVisitor) VisitKey(ctx *grammar.KeyContext) any {
return fieldKeysForName
}
// hasLikeWildcards checks if a value contains LIKE wildcards (% or _)
func hasLikeWildcards(value any) bool {
str, ok := value.(string)
if !ok {
return false
}
return strings.Contains(str, "%") || strings.Contains(str, "_")
}
func trimQuotes(txt string) string {
if len(txt) >= 2 {
if (txt[0] == '"' && txt[len(txt)-1] == '"') ||

View File

@ -0,0 +1,82 @@
package telemetrylogs
import (
"testing"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/stretchr/testify/require"
)
// TestLikeAndILikeWithoutWildcards_Warns Tests that LIKE/ILIKE without wildcards add warnings and include docs URL
func TestLikeAndILikeWithoutWildcards_Warns(t *testing.T) {
fm := NewFieldMapper()
cb := NewConditionBuilder(fm)
keys := buildCompleteFieldKeyMap()
opts := querybuilder.FilterExprVisitorOpts{
Logger: instrumentationtest.New().Logger(),
FieldMapper: fm,
ConditionBuilder: cb,
FieldKeys: keys,
FullTextColumn: DefaultFullTextColumn,
JsonBodyPrefix: BodyJSONStringSearchPrefix,
JsonKeyToKey: GetBodyJSONKey,
}
tests := []string{
"service.name LIKE 'demo-backend'",
"service.name ILIKE 'demo-backend'",
"service.name NOT LIKE 'demo-backend'",
"service.name NOT ILIKE 'demo-backend'",
}
for _, expr := range tests {
t.Run(expr, func(t *testing.T) {
clause, err := querybuilder.PrepareWhereClause(expr, opts)
require.NoError(t, err)
require.NotNil(t, clause)
require.NotEmpty(t, clause.Warnings, "expected warning for: %s", expr)
require.Contains(t, clause.Warnings[0], "operator used without wildcards")
require.Contains(t, clause.WarningsDocURL, "operators-reference/#string-matching-operators")
})
}
}
// TestLikeAndILikeWithWildcards_NoWarn Tests that LIKE/ILIKE with wildcards do not add warnings
func TestLikeAndILikeWithWildcards_NoWarn(t *testing.T) {
fm := NewFieldMapper()
cb := NewConditionBuilder(fm)
keys := buildCompleteFieldKeyMap()
opts := querybuilder.FilterExprVisitorOpts{
Logger: instrumentationtest.New().Logger(),
FieldMapper: fm,
ConditionBuilder: cb,
FieldKeys: keys,
FullTextColumn: DefaultFullTextColumn,
JsonBodyPrefix: BodyJSONStringSearchPrefix,
JsonKeyToKey: GetBodyJSONKey,
}
tests := []string{
"service.name LIKE 'demo-%'",
"service.name LIKE '%demo'",
"service.name ILIKE '_demo'",
"service.name ILIKE '%demo%'",
}
for _, expr := range tests {
t.Run(expr, func(t *testing.T) {
clause, err := querybuilder.PrepareWhereClause(expr, opts)
require.NoError(t, err)
require.NotNil(t, clause)
require.Empty(t, clause.Warnings, "did not expect warnings for: %s", expr)
require.Empty(t, clause.WarningsDocURL)
})
}
}