Skip to content

Commit f2e20a3

Browse files
feat: refine tag negative operators behavior
(cherry picked from commit 5a5d201c1072b3e147287967ff99666d3fd35122)
1 parent 29cc689 commit f2e20a3

2 files changed

Lines changed: 43 additions & 5 deletions

File tree

internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,14 @@ func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionOR(tag
360360
return " and (" + strings.Join(clauses, " OR ") + ") ", queryParams
361361
}
362362

363+
// buildTagFilterPredicate converts one UI tag filter row into a SQL predicate.
364+
// Operator behavior (all case-sensitive):
365+
// - EQUALS: key exists with exact value match.
366+
// - DOES_NOT_EQUAL: key exists with at least one value different from target.
367+
// - CONTAINS: key exists with at least one value containing target substring.
368+
// - DOES_NOT_CONTAIN: key exists with at least one value not containing target substring.
369+
// - EXISTS: key exists.
370+
// - DOES_NOT_EXIST: key does not exist.
363371
func (impl AppListingRepositoryQueryBuilder) buildTagFilterPredicate(tagFilter TagFilter) (string, []interface{}) {
364372
value := ""
365373
if tagFilter.Value != nil {
@@ -370,15 +378,17 @@ func (impl AppListingRepositoryQueryBuilder) buildTagFilterPredicate(tagFilter T
370378
return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)",
371379
[]interface{}{tagFilter.Key, value}
372380
case TagFilterOperatorDoesNotEqual:
373-
// NOT EXISTS intentionally includes apps where the key is missing.
374-
return "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)",
381+
// Best-practice semantics for multi-value keys:
382+
// include app when key exists and at least one value is different from target.
383+
return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value <> ?)",
375384
[]interface{}{tagFilter.Key, value}
376385
case TagFilterOperatorContains:
377386
return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')",
378387
[]interface{}{tagFilter.Key, buildContainsPattern(value)}
379388
case TagFilterOperatorDoesNotContain:
380-
// NOT EXISTS intentionally includes apps where the key is missing.
381-
return "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')",
389+
// Best-practice semantics for multi-value keys:
390+
// include app when key exists and at least one value does not contain target.
391+
return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value NOT LIKE ? ESCAPE '\\')",
382392
[]interface{}{tagFilter.Key, buildContainsPattern(value)}
383393
case TagFilterOperatorExists:
384394
return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)",

internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestBuildAppListingWhereCondition_WithTagFiltersAnd(t *testing.T) {
2424
})
2525

2626
require.Contains(t, whereClause, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)")
27-
require.Contains(t, whereClause, "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')")
27+
require.Contains(t, whereClause, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value NOT LIKE ? ESCAPE '\\')")
2828
require.Contains(t, whereClause, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)")
2929
require.Contains(t, whereClause, "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)")
3030
require.Len(t, queryParams, 8)
@@ -49,6 +49,34 @@ func TestBuildTagFiltersWhereConditionOR(t *testing.T) {
4949
require.Equal(t, []interface{}{"owner", "James", "cost-center", "%ENG%"}, queryParams)
5050
}
5151

52+
func TestBuildTagFilterPredicate_DoesNotEqualRequiresKeyAndDifferentValue(t *testing.T) {
53+
queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar())
54+
value := "mayank"
55+
56+
predicate, queryParams := queryBuilder.buildTagFilterPredicate(TagFilter{
57+
Key: "owner",
58+
Operator: TagFilterOperatorDoesNotEqual,
59+
Value: &value,
60+
})
61+
62+
require.Equal(t, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value <> ?)", predicate)
63+
require.Equal(t, []interface{}{"owner", "mayank"}, queryParams)
64+
}
65+
66+
func TestBuildTagFilterPredicate_DoesNotContainRequiresKeyAndNotLike(t *testing.T) {
67+
queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar())
68+
value := "may"
69+
70+
predicate, queryParams := queryBuilder.buildTagFilterPredicate(TagFilter{
71+
Key: "owner",
72+
Operator: TagFilterOperatorDoesNotContain,
73+
Value: &value,
74+
})
75+
76+
require.Equal(t, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value NOT LIKE ? ESCAPE '\\')", predicate)
77+
require.Equal(t, []interface{}{"owner", "%may%"}, queryParams)
78+
}
79+
5280
func BenchmarkBuildAppListingQueryWithTagFilters(b *testing.B) {
5381
queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar())
5482
tagFilters := make([]TagFilter, 0, 10)

0 commit comments

Comments
 (0)