1
0
mirror of https://github.com/rancher/steve.git synced 2025-09-25 14:49:38 +00:00

Convert tests to real SQL store (#652)

This commit is contained in:
Tom Lebreux
2025-06-02 16:01:45 -06:00
committed by GitHub
parent 97f07399d6
commit a8f3ce48d6
2 changed files with 333 additions and 439 deletions

View File

@@ -946,7 +946,7 @@ func isLabelsFieldList(fields []string) bool {
// toUnstructuredList turns a slice of unstructured objects into an unstructured.UnstructuredList // toUnstructuredList turns a slice of unstructured objects into an unstructured.UnstructuredList
func toUnstructuredList(items []any) *unstructured.UnstructuredList { func toUnstructuredList(items []any) *unstructured.UnstructuredList {
objectItems := make([]map[string]any, len(items)) objectItems := make([]any, len(items))
result := &unstructured.UnstructuredList{ result := &unstructured.UnstructuredList{
Items: make([]unstructured.Unstructured, len(items)), Items: make([]unstructured.Unstructured, len(items)),
Object: map[string]interface{}{"items": objectItems}, Object: map[string]interface{}{"items": objectItems},

View File

@@ -11,21 +11,54 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"reflect"
"strings"
"testing" "testing"
"github.com/rancher/steve/pkg/sqlcache/db" "github.com/rancher/steve/pkg/sqlcache/db"
"github.com/rancher/steve/pkg/sqlcache/encryption"
"github.com/rancher/steve/pkg/sqlcache/partition" "github.com/rancher/steve/pkg/sqlcache/partition"
"github.com/rancher/steve/pkg/sqlcache/sqltypes" "github.com/rancher/steve/pkg/sqlcache/sqltypes"
"github.com/rancher/steve/pkg/sqlcache/store"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/mock/gomock" "go.uber.org/mock/gomock"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/tools/cache"
) )
func makeListOptionIndexer(ctx context.Context, fields [][]string) (*ListOptionIndexer, error) {
gvk := schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "ConfigMap",
}
example := &unstructured.Unstructured{}
example.SetGroupVersionKind(gvk)
name := informerNameFromGVK(gvk)
m, err := encryption.NewManager()
if err != nil {
return nil, err
}
db, err := db.NewClient(nil, m, m)
if err != nil {
return nil, err
}
s, err := store.NewStore(ctx, example, cache.DeletionHandlingMetaNamespaceKeyFunc, db, false, name)
if err != nil {
return nil, err
}
listOptionIndexer, err := NewListOptionIndexer(ctx, fields, s, true)
if err != nil {
return nil, err
}
return listOptionIndexer, nil
}
func TestNewListOptionIndexer(t *testing.T) { func TestNewListOptionIndexer(t *testing.T) {
type testCase struct { type testCase struct {
description string description string
@@ -264,26 +297,103 @@ func TestNewListOptionIndexer(t *testing.T) {
} }
} }
func TestListByOptions(t *testing.T) { func TestNewListOptionIndexerEasy(t *testing.T) {
ctx := context.Background()
type testCase struct { type testCase struct {
description string description string
listOptions sqltypes.ListOptions listOptions sqltypes.ListOptions
partitions []partition.Partition partitions []partition.Partition
ns string ns string
expectedCountStmt string
expectedCountStmtArgs []any items []*unstructured.Unstructured
expectedStmt string
expectedStmtArgs []any extraIndexedFields [][]string
extraIndexedFields []string
expectedList *unstructured.UnstructuredList expectedList *unstructured.UnstructuredList
returnList []any expectedTotal int
expectedContToken string expectedContToken string
expectedErr error expectedErr error
} }
foo := map[string]any{
"metadata": map[string]any{
"name": "obj1",
"namespace": "ns-a",
"somefield": "foo",
"sortfield": "4",
},
}
bar := map[string]any{
"metadata": map[string]any{
"name": "obj2",
"namespace": "ns-a",
"somefield": "bar",
"sortfield": "1",
"labels": map[string]any{
"cows": "milk",
"horses": "saddles",
},
},
}
baz := map[string]any{
"metadata": map[string]any{
"name": "obj3",
"namespace": "ns-a",
"somefield": "baz",
"sortfield": "2",
"labels": map[string]any{
"horses": "saddles",
},
},
"status": map[string]any{
"someotherfield": "helloworld",
},
}
toto := map[string]any{
"metadata": map[string]any{
"name": "obj4",
"namespace": "ns-a",
"somefield": "toto",
"sortfield": "2",
"labels": map[string]any{
"cows": "milk",
},
},
}
lodgePole := map[string]any{
"metadata": map[string]any{
"name": "obj5",
"namespace": "ns-b",
"unknown": "hi",
"labels": map[string]any{
"guard.cattle.io": "lodgepole",
},
},
}
testObject := testStoreObject{Id: "something", Val: "a"} makeList := func(t *testing.T, objs ...map[string]any) *unstructured.UnstructuredList {
unstrTestObjectMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&testObject) t.Helper()
assert.Nil(t, err)
if len(objs) == 0 {
return &unstructured.UnstructuredList{Object: map[string]any{"items": []any{}}, Items: []unstructured.Unstructured{}}
}
var items []any
for _, obj := range objs {
items = append(items, obj)
}
list := &unstructured.Unstructured{
Object: map[string]any{
"items": items,
},
}
itemList, err := list.ToList()
require.NoError(t, err)
return itemList
}
itemList := makeList(t, foo, bar, baz, toto, lodgePole)
var tests []testCase var tests []testCase
tests = append(tests, testCase{ tests = append(tests, testCase{
@@ -291,13 +401,8 @@ func TestListByOptions(t *testing.T) {
listOptions: sqltypes.ListOptions{}, listOptions: sqltypes.ListOptions{},
partitions: []partition.Partition{}, partitions: []partition.Partition{},
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 0,
WHERE
(FALSE)
ORDER BY f."metadata.name" ASC `,
returnList: []any{},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{}}, Items: []unstructured.Unstructured{}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
@@ -308,12 +413,8 @@ func TestListByOptions(t *testing.T) {
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{},
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 0,
WHERE
(FALSE)
ORDER BY f."metadata.name" ASC `,
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{}}, Items: []unstructured.Unstructured{}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
@@ -324,7 +425,7 @@ func TestListByOptions(t *testing.T) {
[]sqltypes.Filter{ []sqltypes.Filter{
{ {
Field: []string{"metadata", "somefield"}, Field: []string{"metadata", "somefield"},
Matches: []string{"somevalue"}, Matches: []string{"foo"},
Op: sqltypes.Eq, Op: sqltypes.Eq,
Partial: true, Partial: true,
}, },
@@ -332,17 +433,10 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, foo),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 1,
WHERE
(f."metadata.somefield" LIKE ? ESCAPE '\') AND
(FALSE)
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"%somevalue%"},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
@@ -353,7 +447,7 @@ func TestListByOptions(t *testing.T) {
[]sqltypes.Filter{ []sqltypes.Filter{
{ {
Field: []string{"metadata", "somefield"}, Field: []string{"metadata", "somefield"},
Matches: []string{"somevalue"}, Matches: []string{"foo"},
Op: sqltypes.NotEq, Op: sqltypes.NotEq,
Partial: true, Partial: true,
}, },
@@ -361,17 +455,10 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, bar, baz, toto, lodgePole),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 4,
WHERE
(f."metadata.somefield" NOT LIKE ? ESCAPE '\') AND
(FALSE)
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"%somevalue%"},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
@@ -382,7 +469,7 @@ func TestListByOptions(t *testing.T) {
[]sqltypes.Filter{ []sqltypes.Filter{
{ {
Field: []string{"metadata", "somefield"}, Field: []string{"metadata", "somefield"},
Matches: []string{"somevalue"}, Matches: []string{"o"},
Op: sqltypes.Eq, Op: sqltypes.Eq,
Partial: true, Partial: true,
}, },
@@ -390,17 +477,10 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, foo, toto),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 2,
WHERE
(f."metadata.somefield" LIKE ? ESCAPE '\') AND
(FALSE)
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"%somevalue%"},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
@@ -411,19 +491,19 @@ func TestListByOptions(t *testing.T) {
[]sqltypes.Filter{ []sqltypes.Filter{
{ {
Field: []string{"metadata", "somefield"}, Field: []string{"metadata", "somefield"},
Matches: []string{"somevalue"}, Matches: []string{"foo"},
Op: sqltypes.Eq, Op: sqltypes.Eq,
Partial: true, Partial: true,
}, },
{ {
Field: []string{"metadata", "somefield"}, Field: []string{"metadata", "somefield"},
Matches: []string{"someothervalue"}, Matches: []string{"bar"},
Op: sqltypes.Eq, Op: sqltypes.Eq,
Partial: true, Partial: true,
}, },
{ {
Field: []string{"metadata", "somefield"}, Field: []string{"metadata", "somefield"},
Matches: []string{"somethirdvalue"}, Matches: []string{"toto"},
Op: sqltypes.NotEq, Op: sqltypes.NotEq,
Partial: true, Partial: true,
}, },
@@ -431,17 +511,10 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, foo, bar, baz, lodgePole),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 4,
WHERE
((f."metadata.somefield" LIKE ? ESCAPE '\') OR (f."metadata.somefield" LIKE ? ESCAPE '\') OR (f."metadata.somefield" NOT LIKE ? ESCAPE '\')) AND
(FALSE)
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"%somevalue%", "%someothervalue%", "%somethirdvalue%"},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
@@ -452,13 +525,13 @@ func TestListByOptions(t *testing.T) {
Filters: []sqltypes.Filter{ Filters: []sqltypes.Filter{
{ {
Field: []string{"metadata", "somefield"}, Field: []string{"metadata", "somefield"},
Matches: []string{"value1"}, Matches: []string{"foo"},
Op: sqltypes.Eq, Op: sqltypes.Eq,
Partial: false, Partial: false,
}, },
{ {
Field: []string{"status", "someotherfield"}, Field: []string{"status", "someotherfield"},
Matches: []string{"value2"}, Matches: []string{"helloworld"},
Op: sqltypes.NotEq, Op: sqltypes.NotEq,
Partial: false, Partial: false,
}, },
@@ -468,7 +541,7 @@ func TestListByOptions(t *testing.T) {
Filters: []sqltypes.Filter{ Filters: []sqltypes.Filter{
{ {
Field: []string{"metadata", "somefield"}, Field: []string{"metadata", "somefield"},
Matches: []string{"value3"}, Matches: []string{"toto"},
Op: sqltypes.Eq, Op: sqltypes.Eq,
Partial: false, Partial: false,
}, },
@@ -476,19 +549,10 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "test4", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, toto),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 1,
WHERE
((f."metadata.somefield" = ?) OR (f."status.someotherfield" != ?)) AND
(f."metadata.somefield" = ?) AND
(f."metadata.namespace" = ?) AND
(FALSE)
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"value1", "value2", "value3", "test4"},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
@@ -507,26 +571,17 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "test41", ns: "",
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, lodgePole),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 1,
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
WHERE
(lt1.label = ? AND lt1.value LIKE ? ESCAPE '\') AND
(f."metadata.namespace" = ?) AND
(FALSE)
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"guard.cattle.io", "%lodgepole%", "test41"},
returnList: []any{},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{}}, Items: []unstructured.Unstructured{}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
tests = append(tests, testCase{ tests = append(tests, testCase{
description: "ListByOptions with two labels filters should use a self-join", description: "ListByOptions with two labels filters should use a self-join",
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{ listOptions: sqltypes.ListOptions{
Filters: []sqltypes.OrFilter{
{ {
Filters: []sqltypes.Filter{ Filters: []sqltypes.Filter{
{ {
@@ -549,25 +604,13 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "test42", ns: "",
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, bar),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 1,
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
LEFT OUTER JOIN "something_labels" lt2 ON o.key = lt2.key
WHERE
(lt1.label = ? AND lt1.value = ?) AND
(lt2.label = ? AND lt2.value = ?) AND
(f."metadata.namespace" = ?) AND
(FALSE)
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"cows", "milk", "horses", "saddles", "test42"},
returnList: []any{},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{}}, Items: []unstructured.Unstructured{}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
tests = append(tests, testCase{ tests = append(tests, testCase{
description: "ListByOptions with a mix of one label and one non-label query can still self-join", description: "ListByOptions with a mix of one label and one non-label query can still self-join",
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{ listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
@@ -575,7 +618,7 @@ func TestListByOptions(t *testing.T) {
Filters: []sqltypes.Filter{ Filters: []sqltypes.Filter{
{ {
Field: []string{"metadata", "labels", "cows"}, Field: []string{"metadata", "labels", "cows"},
Matches: []string{"butter"}, Matches: []string{"milk"},
Op: sqltypes.Eq, Op: sqltypes.Eq,
Partial: false, Partial: false,
}, },
@@ -585,7 +628,7 @@ func TestListByOptions(t *testing.T) {
Filters: []sqltypes.Filter{ Filters: []sqltypes.Filter{
{ {
Field: []string{"metadata", "somefield"}, Field: []string{"metadata", "somefield"},
Matches: []string{"wheat"}, Matches: []string{"toto"},
Op: sqltypes.Eq, Op: sqltypes.Eq,
Partial: false, Partial: false,
}, },
@@ -593,24 +636,13 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "test43", ns: "",
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, toto),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 1,
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
WHERE
(lt1.label = ? AND lt1.value = ?) AND
(f."metadata.somefield" = ?) AND
(f."metadata.namespace" = ?) AND
(FALSE)
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"cows", "butter", "wheat", "test43"},
returnList: []any{},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{}}, Items: []unstructured.Unstructured{}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
tests = append(tests, testCase{ tests = append(tests, testCase{
description: "ListByOptions with only one Sort.Field set should sort on that field only, in ascending order in prepared sql.Stmt", description: "ListByOptions with only one Sort.Field set should sort on that field only, in ascending order in prepared sql.Stmt",
listOptions: sqltypes.ListOptions{ listOptions: sqltypes.ListOptions{
@@ -623,21 +655,13 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "test5", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, lodgePole, bar, baz, foo, toto),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 5,
WHERE
(f."metadata.namespace" = ?) AND
(FALSE)
ORDER BY f."metadata.somefield" ASC`,
expectedStmtArgs: []any{"test5"},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
tests = append(tests, testCase{ tests = append(tests, testCase{
description: "sort one field descending", description: "sort one field descending",
listOptions: sqltypes.ListOptions{ listOptions: sqltypes.ListOptions{
@@ -650,164 +674,132 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "test5a", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, toto, foo, baz, bar, lodgePole),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 5,
WHERE
(f."metadata.namespace" = ?) AND
(FALSE)
ORDER BY f."metadata.somefield" DESC`,
expectedStmtArgs: []any{"test5a"},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
tests = append(tests, testCase{ tests = append(tests, testCase{
description: "sort one unbound label descending", description: "sort one unbound field descending",
listOptions: sqltypes.ListOptions{ listOptions: sqltypes.ListOptions{
SortList: sqltypes.SortList{ SortList: sqltypes.SortList{
SortDirectives: []sqltypes.Sort{ SortDirectives: []sqltypes.Sort{
{ {
Fields: []string{"metadata", "labels", "flip"}, Fields: []string{"metadata", "unknown"},
Order: sqltypes.DESC, Order: sqltypes.DESC,
}, },
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "test5a",
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
JOIN "something_fields" f ON o.key = f.key
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
WHERE
(lt1.label = ?) AND
(f."metadata.namespace" = ?) AND
(FALSE)
ORDER BY (CASE lt1.label WHEN ? THEN lt1.value ELSE NULL END) DESC NULLS FIRST`,
expectedStmtArgs: []any{"flip", "test5a", "flip"},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "",
expectedErr: nil,
})
tests = append(tests, testCase{
description: "ListByOptions sorting on two complex fields should sort on the first field in ascending order first and then sort on the second labels field in ascending order in prepared sql.Stmt",
listOptions: sqltypes.ListOptions{
SortList: sqltypes.SortList{
SortDirectives: []sqltypes.Sort{
{
Fields: []string{"metadata", "fields", "3"},
Order: sqltypes.ASC,
},
{
Fields: []string{"metadata", "labels", "stub.io/candy"},
Order: sqltypes.ASC,
},
},
},
},
extraIndexedFields: []string{"metadata.fields[3]", "metadata.labels[stub.io/candy]"},
partitions: []partition.Partition{},
ns: "", ns: "",
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, lodgePole, toto, baz, bar, foo),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 5,
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
WHERE
(lt1.label = ?) AND
(FALSE)
ORDER BY f."metadata.fields[3]" ASC, (CASE lt1.label WHEN ? THEN lt1.value ELSE NULL END) ASC NULLS LAST`,
expectedStmtArgs: []any{"stub.io/candy", "stub.io/candy"},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
// tests = append(tests, testCase{
// description: "sort one unbound label descending",
// listOptions: sqltypes.ListOptions{
// SortList: sqltypes.SortList{
// SortDirectives: []sqltypes.Sort{
// {
// Fields: []string{"metadata", "labels", "flip"},
// Order: sqltypes.DESC,
// },
// },
// },
// },
// partitions: []partition.Partition{{All: true}},
// ns: "",
// expectedList: makeList(t, lodgePole, toto, baz, bar, foo),
// expectedTotal: 5,
// expectedContToken: "",
// expectedErr: nil,
// })
// tests = append(tests, testCase{
// description: "ListByOptions sorting on two complex fields should sort on the first field in ascending order first and then sort on the second labels field in ascending order in prepared sql.Stmt",
// listOptions: sqltypes.ListOptions{
// SortList: sqltypes.SortList{
// SortDirectives: []sqltypes.Sort{
// {
// Fields: []string{"metadata", "sortfield"},
// Order: sqltypes.ASC,
// },
// {
// Fields: []string{"metadata", "labels", "cows"},
// Order: sqltypes.ASC,
// },
// },
// },
// },
// partitions: []partition.Partition{{All: true}},
// ns: "",
// expectedList: makeList(t),
// expectedTotal: 5,
// expectedContToken: "",
// expectedErr: nil,
// })
tests = append(tests, testCase{ tests = append(tests, testCase{
description: "ListByOptions sorting on two fields should sort on the first field in ascending order first and then sort on the second field in ascending order in prepared sql.Stmt", description: "ListByOptions sorting on two fields should sort on the first field in ascending order first and then sort on the second field in ascending order in prepared sql.Stmt",
listOptions: sqltypes.ListOptions{ listOptions: sqltypes.ListOptions{
SortList: sqltypes.SortList{ SortList: sqltypes.SortList{
SortDirectives: []sqltypes.Sort{ SortDirectives: []sqltypes.Sort{
{ {
Fields: []string{"metadata", "somefield"}, Fields: []string{"metadata", "sortfield"},
Order: sqltypes.ASC, Order: sqltypes.ASC,
}, },
{ {
Fields: []string{"status", "someotherfield"}, Fields: []string{"metadata", "somefield"},
Order: sqltypes.ASC, Order: sqltypes.ASC,
}, },
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, lodgePole, bar, baz, toto, foo),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 5,
WHERE
(FALSE)
ORDER BY f."metadata.somefield" ASC, f."status.someotherfield" ASC`,
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
tests = append(tests, testCase{ tests = append(tests, testCase{
description: "ListByOptions sorting on two fields should sort on the first field in descending order first and then sort on the second field in ascending order in prepared sql.Stmt", description: "ListByOptions sorting on two fields should sort on the first field in descending order first and then sort on the second field in ascending order in prepared sql.Stmt",
listOptions: sqltypes.ListOptions{ listOptions: sqltypes.ListOptions{
SortList: sqltypes.SortList{ SortList: sqltypes.SortList{
SortDirectives: []sqltypes.Sort{ SortDirectives: []sqltypes.Sort{
{ {
Fields: []string{"metadata", "somefield"}, Fields: []string{"metadata", "sortfield"},
Order: sqltypes.DESC, Order: sqltypes.DESC,
}, },
{ {
Fields: []string{"status", "someotherfield"}, Fields: []string{"metadata", "somefield"},
Order: sqltypes.ASC, Order: sqltypes.ASC,
}, },
}, },
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, foo, baz, toto, bar, lodgePole),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 5,
WHERE
(FALSE)
ORDER BY f."metadata.somefield" DESC, f."status.someotherfield" ASC`,
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
tests = append(tests, testCase{ tests = append(tests, testCase{
description: "ListByOptions with Pagination.PageSize set should set limit to PageSize in prepared sql.Stmt", description: "ListByOptions with Pagination.PageSize set should set limit to PageSize in prepared sql.Stmt",
listOptions: sqltypes.ListOptions{ listOptions: sqltypes.ListOptions{
Pagination: sqltypes.Pagination{ Pagination: sqltypes.Pagination{
PageSize: 10, PageSize: 3,
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, foo, bar, baz),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 5,
WHERE expectedContToken: "3",
(FALSE)
ORDER BY f."metadata.name" ASC
LIMIT ?`,
expectedStmtArgs: []any{10},
expectedCountStmt: `SELECT COUNT(*) FROM (SELECT o.object, o.objectnonce, o.dekid FROM "something" o
JOIN "something_fields" f ON o.key = f.key
WHERE
(FALSE))`,
expectedCountStmtArgs: []any{},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
tests = append(tests, testCase{ tests = append(tests, testCase{
@@ -817,67 +809,27 @@ func TestListByOptions(t *testing.T) {
Page: 2, Page: 2,
}, },
}, },
partitions: []partition.Partition{}, partitions: []partition.Partition{{All: true}},
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, foo, bar, baz, toto, lodgePole),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 5,
WHERE
(FALSE)
ORDER BY f."metadata.name" ASC `,
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "",
expectedErr: nil,
})
tests = append(tests, testCase{
description: "ListByOptions with Pagination.Page and PageSize set limit to PageSize and offset to PageSize * (Page - 1) in prepared sql.Stmt",
listOptions: sqltypes.ListOptions{
Pagination: sqltypes.Pagination{
PageSize: 10,
Page: 2,
},
},
partitions: []partition.Partition{},
ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
JOIN "something_fields" f ON o.key = f.key
WHERE
(FALSE)
ORDER BY f."metadata.name" ASC
LIMIT ?
OFFSET ?`,
expectedStmtArgs: []any{10, 10},
expectedCountStmt: `SELECT COUNT(*) FROM (SELECT o.object, o.objectnonce, o.dekid FROM "something" o
JOIN "something_fields" f ON o.key = f.key
WHERE
(FALSE))`,
expectedCountStmtArgs: []any{},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "",
expectedErr: nil,
})
tests = append(tests, testCase{
description: "ListByOptions with a Namespace Partition should select only items where metadata.namespace is equal to Namespace and all other conditions are met in prepared sql.Stmt",
partitions: []partition.Partition{
{
Namespace: "somens",
},
},
ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
JOIN "something_fields" f ON o.key = f.key
WHERE
(f."metadata.namespace" = ? AND FALSE)
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"somens"},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
// tests = append(tests, testCase{
// description: "ListByOptions with a Namespace Partition should select only items where metadata.namespace is equal to Namespace and all other conditions are met in prepared sql.Stmt",
// partitions: []partition.Partition{
// {
// Namespace: "ns-b",
// },
// },
// // XXX: Why do I need to specify the namespace here too?
// ns: "ns-b",
// expectedList: makeList(t, lodgePole),
// expectedTotal: 1,
// expectedContToken: "",
// expectedErr: nil,
// })
tests = append(tests, testCase{ tests = append(tests, testCase{
description: "ListByOptions with a All Partition should select all items that meet all other conditions in prepared sql.Stmt", description: "ListByOptions with a All Partition should select all items that meet all other conditions in prepared sql.Stmt",
partitions: []partition.Partition{ partitions: []partition.Partition{
@@ -886,11 +838,8 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, foo, bar, baz, toto, lodgePole),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 5,
ORDER BY f."metadata.name" ASC `,
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
@@ -902,11 +851,8 @@ func TestListByOptions(t *testing.T) {
}, },
}, },
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, foo, bar, baz, toto, lodgePole),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 5,
ORDER BY f."metadata.name" ASC `,
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
@@ -914,95 +860,43 @@ func TestListByOptions(t *testing.T) {
description: "ListByOptions with a Names Partition should select only items where metadata.name equals an items in Names and all other conditions are met in prepared sql.Stmt", description: "ListByOptions with a Names Partition should select only items where metadata.name equals an items in Names and all other conditions are met in prepared sql.Stmt",
partitions: []partition.Partition{ partitions: []partition.Partition{
{ {
Names: sets.New[string]("someid", "someotherid"), Names: sets.New("obj1", "obj2"),
}, },
}, },
ns: "", ns: "",
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o expectedList: makeList(t, foo, bar),
JOIN "something_fields" f ON o.key = f.key expectedTotal: 2,
WHERE
(f."metadata.name" IN (?, ?))
ORDER BY f."metadata.name" ASC `,
expectedStmtArgs: []any{"someid", "someotherid"},
returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}},
expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}},
expectedContToken: "", expectedContToken: "",
expectedErr: nil, expectedErr: nil,
}) })
t.Parallel() t.Parallel()
for _, test := range tests { for _, test := range tests {
t.Run(test.description, func(t *testing.T) { t.Run(test.description, func(t *testing.T) {
txClient := NewMockTXClient(gomock.NewController(t)) fields := [][]string{
store := NewMockStore(gomock.NewController(t)) {"metadata", "somefield"},
stmts := NewMockStmt(gomock.NewController(t)) {"status", "someotherfield"},
i := &Indexer{ {"metadata", "unknown"},
Store: store, {"metadata", "sortfield"},
} }
lii := &ListOptionIndexer{ fields = append(fields, test.extraIndexedFields...)
Indexer: i,
indexedFields: []string{"metadata.somefield", "status.someotherfield"}, loi, err := makeListOptionIndexer(ctx, fields)
assert.NoError(t, err)
for _, item := range itemList.Items {
err = loi.Add(&item)
assert.NoError(t, err)
} }
if len(test.extraIndexedFields) > 0 {
lii.indexedFields = append(lii.indexedFields, test.extraIndexedFields...) list, total, contToken, err := loi.ListByOptions(ctx, &test.listOptions, test.partitions, test.ns)
}
queryInfo, err := lii.constructQuery(&test.listOptions, test.partitions, test.ns, "something")
if test.expectedErr != nil { if test.expectedErr != nil {
assert.Equal(t, test.expectedErr, err) assert.Error(t, err)
return return
} }
assert.Nil(t, err)
assert.Equal(t, test.expectedStmt, queryInfo.query)
if test.expectedStmtArgs == nil {
test.expectedStmtArgs = []any{}
}
assert.Equal(t, test.expectedStmtArgs, queryInfo.params)
assert.Equal(t, test.expectedCountStmt, queryInfo.countQuery)
assert.Equal(t, test.expectedCountStmtArgs, queryInfo.countParams)
stmt := &sql.Stmt{}
rows := &sql.Rows{}
objType := reflect.TypeOf(testObject)
txClient.EXPECT().Stmt(gomock.Any()).Return(stmts).AnyTimes()
store.EXPECT().Prepare(test.expectedStmt).Do(func(a ...any) {
fmt.Println(a)
}).Return(stmt)
if args := test.expectedStmtArgs; args != nil {
stmts.EXPECT().QueryContext(gomock.Any(), gomock.Any()).Return(rows, nil).AnyTimes()
} else if strings.Contains(test.expectedStmt, "LIMIT") {
stmts.EXPECT().QueryContext(gomock.Any(), args...).Return(rows, nil)
txClient.EXPECT().Stmt(gomock.Any()).Return(stmts)
stmts.EXPECT().QueryContext(gomock.Any()).Return(rows, nil)
} else {
stmts.EXPECT().QueryContext(gomock.Any()).Return(rows, nil)
}
store.EXPECT().GetType().Return(objType)
store.EXPECT().GetShouldEncrypt().Return(false)
store.EXPECT().ReadObjects(rows, objType, false).Return(test.returnList, nil)
store.EXPECT().CloseStmt(stmt).Return(nil)
store.EXPECT().WithTransaction(gomock.Any(), false, gomock.Any()).Return(nil).Do(
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
err := f(txClient)
if test.expectedErr == nil {
assert.Nil(t, err)
} else {
assert.Equal(t, test.expectedErr, err)
}
})
if test.expectedCountStmt != "" {
store.EXPECT().Prepare(test.expectedCountStmt).Return(stmt)
store.EXPECT().ReadInt(rows).Return(len(test.expectedList.Items), nil)
store.EXPECT().CloseStmt(stmt).Return(nil)
}
list, total, contToken, err := lii.executeQuery(context.Background(), queryInfo)
if test.expectedErr == nil {
assert.Nil(t, err)
} else {
assert.Equal(t, test.expectedErr, err)
}
assert.Equal(t, test.expectedList, list) assert.Equal(t, test.expectedList, list)
assert.Equal(t, len(test.expectedList.Items), total) assert.Equal(t, test.expectedTotal, total)
assert.Equal(t, test.expectedContToken, contToken) assert.Equal(t, test.expectedContToken, contToken)
}) })
} }