Skip to content

Commit 5aff419

Browse files
Add a contains operator to the VAI filter language (#1002)
* Implement and test code-gen for a CONTAINS operator. * Lex and parse a CONTAINS operator. * Add the 'hasBarredValue' custom sql function. * Add integration tests for the CONTAINS operator. * Add integration tests that verify 'contains' doesn't pick substrings * Add the NOTCONTAINS operator. Also: - Simplify code-gen for `metadata.labels.... CONTAINS ...` - Because labels can't have '|' characters, CONTAINS reduces to == * Add integration tests for the CONTAINS op.
1 parent e4b5b79 commit 5aff419

File tree

13 files changed

+644
-30
lines changed

13 files changed

+644
-30
lines changed

pkg/sqlcache/db/client.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"os"
1919
"reflect"
2020
"regexp"
21+
"slices"
2122
"strconv"
2223
"strings"
2324
"sync"
@@ -544,6 +545,7 @@ func (c *client) NewConnection(useTempDir bool) (string, error) {
544545
return dbPath, err
545546
}
546547
sqlite.RegisterDeterministicScalarFunction("extractBarredValue", 2, extractBarredValue)
548+
sqlite.RegisterDeterministicScalarFunction("hasBarredValue", 2, hasBarredValue)
547549
sqlite.RegisterDeterministicScalarFunction("inet_aton", 1, inetAtoN)
548550
sqlite.RegisterDeterministicScalarFunction("memoryInBytes", 1, memoryInBytes)
549551
c.conn = &connection{sqlDB}
@@ -573,7 +575,7 @@ func extractBarredValue(ctx *sqlite.FunctionContext, args []driver.Value) (drive
573575
return nil, fmt.Errorf("unsupported type for arg2: expected an int, got: %T", args[0])
574576
}
575577
if err != nil {
576-
return nil, fmt.Errorf("problem with arg2: %w", err)
578+
return nil, fmt.Errorf("extractBarredValue: problem with arg2: %w", err)
577579
}
578580
parts := strings.Split(arg1, "|")
579581
if arg2 >= len(parts) || arg2 < 0 {
@@ -582,6 +584,29 @@ func extractBarredValue(ctx *sqlite.FunctionContext, args []driver.Value) (drive
582584
return parts[arg2], nil
583585
}
584586

587+
func hasBarredValue(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
588+
var arg1 string
589+
var arg2 string
590+
switch argTyped := args[0].(type) {
591+
case string:
592+
arg1 = argTyped
593+
case []byte:
594+
arg1 = string(argTyped)
595+
default:
596+
return nil, fmt.Errorf("hasBarredValue: unsupported type for arg1: expected a string, got :%T", args[0])
597+
}
598+
switch argTyped := args[1].(type) {
599+
case string:
600+
arg2 = argTyped
601+
case []byte:
602+
arg2 = string(argTyped)
603+
default:
604+
return nil, fmt.Errorf("hasBarredValue: unsupported type for arg2: expected a string, got: %T", args[0])
605+
}
606+
parts := strings.Split(arg1, "|")
607+
return slices.Contains(parts, arg2), nil
608+
}
609+
585610
func inetAtoN(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
586611
var arg1 string
587612
switch argTyped := args[0].(type) {

pkg/sqlcache/informer/sqlgenerator.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,8 @@ SELECT key, value FROM "%s_labels"
887887
// KEY ! [] # ,!KEY, => assert KEY doesn't exist
888888
// KEY in VALUES
889889
// KEY notin VALUES
890+
// KEY contains VALUES
891+
// KEY notcontains VALUES
890892

891893
func (l *ListOptionIndexer) getFieldFilter(filter sqltypes.Filter, prefix string) (string, []any, error) {
892894
opString := ""
@@ -946,6 +948,24 @@ func (l *ListOptionIndexer) getFieldFilter(filter sqltypes.Filter, prefix string
946948
matches[i] = match
947949
}
948950
return clause, matches, nil
951+
952+
case sqltypes.Contains:
953+
if len(filter.Matches) != 1 {
954+
return "", nil, fmt.Errorf("array checking works on exactly one field, %d were specified", len(filter.Matches))
955+
}
956+
clause := fmt.Sprintf("hasBarredValue(%s, ?)", fieldEntry)
957+
matches := make([]any, 1)
958+
matches[0] = filter.Matches[0]
959+
return clause, matches, nil
960+
961+
case sqltypes.NotContains:
962+
if len(filter.Matches) != 1 {
963+
return "", nil, fmt.Errorf("array checking works on exactly one field, %d were specified", len(filter.Matches))
964+
}
965+
clause := fmt.Sprintf("NOT hasBarredValue(%s, ?)", fieldEntry)
966+
matches := make([]any, 1)
967+
matches[0] = filter.Matches[0]
968+
return clause, matches, nil
949969
}
950970

951971
return "", nil, fmt.Errorf("unrecognized operator: %s", opString)
@@ -1052,6 +1072,22 @@ func (l *ListOptionIndexer) getLabelFilter(index int, filter sqltypes.Filter, ma
10521072
matches = append(matches, match)
10531073
}
10541074
return clause, matches, nil
1075+
1076+
case sqltypes.Contains:
1077+
if len(filter.Matches) != 1 {
1078+
return "", nil, fmt.Errorf("array checking works on exactly one field, %d were specified", len(filter.Matches))
1079+
}
1080+
// Labels can't have | characters so they're implemented like '='
1081+
filter.Op = sqltypes.Eq
1082+
return l.getLabelFilter(index, filter, mainFieldPrefix, isSummaryFilter, dbName)
1083+
1084+
case sqltypes.NotContains:
1085+
if len(filter.Matches) != 1 {
1086+
return "", nil, fmt.Errorf("array checking works on exactly one field, %d were specified", len(filter.Matches))
1087+
}
1088+
// Labels can't have | characters so they're implemented like '='
1089+
filter.Op = sqltypes.NotEq
1090+
return l.getLabelFilter(index, filter, mainFieldPrefix, isSummaryFilter, dbName)
10551091
}
10561092
return "", nil, fmt.Errorf("unrecognized operator: %s", opString)
10571093
}

pkg/sqlcache/informer/sqlgenerator_test.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2566,6 +2566,268 @@ SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
25662566
}
25672567
}
25682568

2569+
func TestConstructQueryWithContainsOp(t *testing.T) {
2570+
type testCase struct {
2571+
description string
2572+
listOptions sqltypes.ListOptions
2573+
partitions []partition.Partition
2574+
ns string
2575+
expectedCountStmt string
2576+
expectedCountStmtArgs []any
2577+
expectedStmt string
2578+
expectedStmtArgs []any
2579+
expectedErr string
2580+
}
2581+
2582+
var tests []testCase
2583+
tests = append(tests, testCase{
2584+
description: "TestConstructQueryWithContainsOp: handles CONTAIN statements on indexed arrays",
2585+
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
2586+
{
2587+
[]sqltypes.Filter{
2588+
{
2589+
Matches: []string{"needle01"},
2590+
Field: []string{"metadata", "fields"},
2591+
Op: sqltypes.Contains,
2592+
},
2593+
},
2594+
},
2595+
},
2596+
},
2597+
partitions: []partition.Partition{},
2598+
ns: "",
2599+
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
2600+
JOIN "something_fields" f ON o.key = f.key
2601+
WHERE
2602+
(hasBarredValue(f."metadata.fields", ?)) AND
2603+
(FALSE)
2604+
ORDER BY f."metadata.name" ASC`,
2605+
expectedStmtArgs: []any{"needle01"},
2606+
})
2607+
tests = append(tests, testCase{
2608+
description: "TestConstructQueryWithContainsOp: handles CONTAIN statements on single strings",
2609+
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
2610+
{
2611+
[]sqltypes.Filter{
2612+
{
2613+
Matches: []string{"needle02"},
2614+
Field: []string{"metadata", "queryField1"},
2615+
Op: sqltypes.Contains,
2616+
},
2617+
},
2618+
},
2619+
},
2620+
},
2621+
partitions: []partition.Partition{},
2622+
ns: "",
2623+
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
2624+
JOIN "something_fields" f ON o.key = f.key
2625+
WHERE
2626+
(hasBarredValue(f."metadata.queryField1", ?)) AND
2627+
(FALSE)
2628+
ORDER BY f."metadata.name" ASC`,
2629+
expectedStmtArgs: []any{"needle02"},
2630+
})
2631+
tests = append(tests, testCase{
2632+
description: "TestConstructQueryWithContainsOp: error CONTAIN statements on too many target strings",
2633+
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
2634+
{
2635+
[]sqltypes.Filter{
2636+
{
2637+
Matches: []string{"too", "many", "targets"},
2638+
Field: []string{"metadata", "queryField1"},
2639+
Op: sqltypes.Contains,
2640+
},
2641+
},
2642+
},
2643+
},
2644+
},
2645+
ns: "",
2646+
expectedErr: "array checking works on exactly one field, 3 were specified",
2647+
})
2648+
tests = append(tests, testCase{
2649+
description: "TestConstructQueryWithContainsOp: error CONTAIN statements on unrecognized field",
2650+
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
2651+
{
2652+
[]sqltypes.Filter{
2653+
{
2654+
Matches: []string{"this"},
2655+
Field: []string{"bills", "farm"},
2656+
Op: sqltypes.Contains,
2657+
},
2658+
},
2659+
},
2660+
},
2661+
},
2662+
ns: "",
2663+
expectedErr: "column is invalid [bills.farm]: supplied column is invalid",
2664+
})
2665+
tests = append(tests, testCase{
2666+
description: "TestConstructQueryWithContainsOp: error CONTAIN statements on no target string",
2667+
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
2668+
{
2669+
[]sqltypes.Filter{
2670+
{
2671+
Field: []string{"metadata", "queryField1"},
2672+
Op: sqltypes.Contains,
2673+
},
2674+
},
2675+
},
2676+
},
2677+
},
2678+
ns: "",
2679+
expectedErr: "array checking works on exactly one field, 0 were specified",
2680+
})
2681+
tests = append(tests, testCase{
2682+
description: "TestConstructQueryWithContainsOp: handles NOTCONTAINS statements on indexed arrays",
2683+
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
2684+
{
2685+
[]sqltypes.Filter{
2686+
{
2687+
Matches: []string{"needle01"},
2688+
Field: []string{"metadata", "fields"},
2689+
Op: sqltypes.NotContains,
2690+
},
2691+
},
2692+
},
2693+
},
2694+
},
2695+
partitions: []partition.Partition{},
2696+
ns: "",
2697+
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
2698+
JOIN "something_fields" f ON o.key = f.key
2699+
WHERE
2700+
(NOT hasBarredValue(f."metadata.fields", ?)) AND
2701+
(FALSE)
2702+
ORDER BY f."metadata.name" ASC`,
2703+
expectedStmtArgs: []any{"needle01"},
2704+
})
2705+
tests = append(tests, testCase{
2706+
description: "TestConstructQueryWithContainsOp: handles NOTCONTAINS statements on single strings",
2707+
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
2708+
{
2709+
[]sqltypes.Filter{
2710+
{
2711+
Matches: []string{"needle02"},
2712+
Field: []string{"metadata", "queryField1"},
2713+
Op: sqltypes.NotContains,
2714+
},
2715+
},
2716+
},
2717+
},
2718+
},
2719+
partitions: []partition.Partition{},
2720+
ns: "",
2721+
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
2722+
JOIN "something_fields" f ON o.key = f.key
2723+
WHERE
2724+
(NOT hasBarredValue(f."metadata.queryField1", ?)) AND
2725+
(FALSE)
2726+
ORDER BY f."metadata.name" ASC`,
2727+
expectedStmtArgs: []any{"needle02"},
2728+
})
2729+
tests = append(tests, testCase{
2730+
description: "TestConstructQueryWithContainsOp: error NOTCONTAINS statements on too many target strings",
2731+
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
2732+
{
2733+
[]sqltypes.Filter{
2734+
{
2735+
Matches: []string{"too", "many", "targets"},
2736+
Field: []string{"metadata", "queryField1"},
2737+
Op: sqltypes.NotContains,
2738+
},
2739+
},
2740+
},
2741+
},
2742+
},
2743+
ns: "",
2744+
expectedErr: "array checking works on exactly one field, 3 were specified",
2745+
})
2746+
tests = append(tests, testCase{
2747+
description: "TestConstructQueryWithContainsOp: error NOTCONTAINS statements on unrecognized field",
2748+
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
2749+
{
2750+
[]sqltypes.Filter{
2751+
{
2752+
Matches: []string{"this"},
2753+
Field: []string{"bills", "farm"},
2754+
Op: sqltypes.NotContains,
2755+
},
2756+
},
2757+
},
2758+
},
2759+
},
2760+
ns: "",
2761+
expectedErr: "column is invalid [bills.farm]: supplied column is invalid",
2762+
})
2763+
tests = append(tests, testCase{
2764+
description: "TestConstructQueryWithContainsOp: error NOTCONTAINS statements on no target string",
2765+
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
2766+
{
2767+
[]sqltypes.Filter{
2768+
{
2769+
Field: []string{"metadata", "queryField1"},
2770+
Op: sqltypes.NotContains,
2771+
},
2772+
},
2773+
},
2774+
},
2775+
},
2776+
ns: "",
2777+
expectedErr: "array checking works on exactly one field, 0 were specified",
2778+
})
2779+
tests = append(tests, testCase{
2780+
description: "TestConstructQueryWithContainsOp: handles label: NOTCONTAINS statements on single strings",
2781+
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
2782+
{
2783+
[]sqltypes.Filter{
2784+
{
2785+
Matches: []string{"needle03"},
2786+
Field: []string{"metadata", "labels", "sewingSupplies"},
2787+
Op: sqltypes.NotContains,
2788+
},
2789+
},
2790+
},
2791+
},
2792+
},
2793+
partitions: []partition.Partition{},
2794+
ns: "",
2795+
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
2796+
JOIN "something_fields" f ON o.key = f.key
2797+
LEFT OUTER JOIN "something_labels" lt1 ON f.key = lt1.key
2798+
WHERE
2799+
((o.key NOT IN (SELECT f1.key FROM "something_fields" f1
2800+
LEFT OUTER JOIN "something_labels" lt1i1 ON f1.key = lt1i1.key
2801+
WHERE lt1i1.label = ?)) OR (lt1.label = ? AND lt1.value != ?)) AND
2802+
(FALSE)
2803+
ORDER BY f."metadata.name" ASC`,
2804+
expectedStmtArgs: []any{"sewingSupplies", "sewingSupplies", "needle03"},
2805+
})
2806+
t.Parallel()
2807+
for _, test := range tests {
2808+
t.Run(test.description, func(t *testing.T) {
2809+
store := NewMockStore(gomock.NewController(t))
2810+
i := &Indexer{
2811+
Store: store,
2812+
}
2813+
lii := &ListOptionIndexer{
2814+
Indexer: i,
2815+
indexedFields: toIndexedFieldsFromColumnNames("metadata.queryField1", "status.queryField2", "metadata.fields"),
2816+
}
2817+
queryInfo, err := lii.constructQuery(&test.listOptions, test.partitions, test.ns, "something")
2818+
if test.expectedErr != "" {
2819+
assert.Equal(t, test.expectedErr, err.Error())
2820+
return
2821+
}
2822+
require.Nil(t, err)
2823+
assert.Equal(t, test.expectedStmt, queryInfo.query)
2824+
assert.Equal(t, test.expectedStmtArgs, queryInfo.params)
2825+
assert.Equal(t, test.expectedCountStmt, queryInfo.countQuery)
2826+
assert.Equal(t, test.expectedCountStmtArgs, queryInfo.countParams)
2827+
})
2828+
}
2829+
}
2830+
25692831
func TestConstructSummaryQueryForField(t *testing.T) {
25702832
type testCase struct {
25712833
description string

0 commit comments

Comments
 (0)