diff --git a/internal/connor/all.go b/internal/connor/all.go new file mode 100644 index 0000000000..0b9800de89 --- /dev/null +++ b/internal/connor/all.go @@ -0,0 +1,53 @@ +package connor + +import ( + "github.com/sourcenetwork/defradb/client" + + "github.com/sourcenetwork/immutable" +) + +// all is an operator which allows the evaluation of +// a number of conditions over a list of values +// matching if all of them match. +func all(condition, data any) (bool, error) { + switch t := data.(type) { + case []string: + return allSlice(condition, t) + + case []immutable.Option[string]: + return allSlice(condition, t) + + case []int64: + return allSlice(condition, t) + + case []immutable.Option[int64]: + return allSlice(condition, t) + + case []bool: + return allSlice(condition, t) + + case []immutable.Option[bool]: + return allSlice(condition, t) + + case []float64: + return allSlice(condition, t) + + case []immutable.Option[float64]: + return allSlice(condition, t) + + default: + return false, client.NewErrUnhandledType("data", data) + } +} + +func allSlice[T any](condition any, data []T) (bool, error) { + for _, c := range data { + m, err := eq(condition, c) + if err != nil { + return false, err + } else if !m { + return false, nil + } + } + return true, nil +} diff --git a/internal/connor/and.go b/internal/connor/and.go index 054718ee6a..be2e097309 100644 --- a/internal/connor/and.go +++ b/internal/connor/and.go @@ -14,8 +14,8 @@ func and(condition, data any) (bool, error) { return false, nil } } - return true, nil + default: return false, client.NewErrUnhandledType("condition", cn) } diff --git a/internal/connor/any.go b/internal/connor/any.go new file mode 100644 index 0000000000..a9c02b1369 --- /dev/null +++ b/internal/connor/any.go @@ -0,0 +1,53 @@ +package connor + +import ( + "github.com/sourcenetwork/defradb/client" + + "github.com/sourcenetwork/immutable" +) + +// anyOp is an operator which allows the evaluation of +// a number of conditions over a list of values +// matching if any of them match. +func anyOp(condition, data any) (bool, error) { + switch t := data.(type) { + case []string: + return anySlice(condition, t) + + case []immutable.Option[string]: + return anySlice(condition, t) + + case []int64: + return anySlice(condition, t) + + case []immutable.Option[int64]: + return anySlice(condition, t) + + case []bool: + return anySlice(condition, t) + + case []immutable.Option[bool]: + return anySlice(condition, t) + + case []float64: + return anySlice(condition, t) + + case []immutable.Option[float64]: + return anySlice(condition, t) + + default: + return false, client.NewErrUnhandledType("data", data) + } +} + +func anySlice[T any](condition any, data []T) (bool, error) { + for _, c := range data { + m, err := eq(condition, c) + if err != nil { + return false, err + } else if m { + return true, nil + } + } + return false, nil +} diff --git a/internal/connor/connor.go b/internal/connor/connor.go index 927b1dfffd..086ba0cd49 100644 --- a/internal/connor/connor.go +++ b/internal/connor/connor.go @@ -18,6 +18,10 @@ func matchWith(op string, conditions, data any) (bool, error) { switch op { case "_and": return and(conditions, data) + case "_any": + return anyOp(conditions, data) + case "_all": + return all(conditions, data) case "_eq": return eq(conditions, data) case "_ge": @@ -44,6 +48,8 @@ func matchWith(op string, conditions, data any) (bool, error) { return ilike(conditions, data) case "_nilike": return nilike(conditions, data) + case "_none": + return none(conditions, data) case "_not": return not(conditions, data) default: diff --git a/internal/connor/eq.go b/internal/connor/eq.go index 86888eef37..3f849348b8 100644 --- a/internal/connor/eq.go +++ b/internal/connor/eq.go @@ -16,74 +16,59 @@ import ( func eq(condition, data any) (bool, error) { switch arr := data.(type) { case []core.Doc: - for _, item := range arr { - m, err := eq(condition, item) - if err != nil { - return false, err - } - - if m { - return true, nil - } - } - return false, nil + return anySlice(condition, arr) case immutable.Option[bool]: - if !arr.HasValue() { - return condition == nil, nil - } - data = arr.Value() + data = immutableValueOrNil(arr) case immutable.Option[int64]: - if !arr.HasValue() { - return condition == nil, nil - } - data = arr.Value() + data = immutableValueOrNil(arr) case immutable.Option[float64]: - if !arr.HasValue() { - return condition == nil, nil - } - data = arr.Value() + data = immutableValueOrNil(arr) case immutable.Option[string]: - if !arr.HasValue() { - return condition == nil, nil - } - data = arr.Value() + data = immutableValueOrNil(arr) } switch cn := condition.(type) { + case map[FilterKey]any: + for prop, cond := range cn { + m, err := matchWith(prop.GetOperatorOrDefault("_eq"), cond, prop.GetProp(data)) + if err != nil { + return false, err + } else if !m { + return false, nil + } + } + return true, nil + case string: if d, ok := data.(string); ok { return d == cn, nil } return false, nil + case int64: return numbers.Equal(cn, data), nil + case int32: return numbers.Equal(cn, data), nil + case float64: return numbers.Equal(cn, data), nil - case map[FilterKey]any: - m := true - for prop, cond := range cn { - var err error - m, err = matchWith(prop.GetOperatorOrDefault("_eq"), cond, prop.GetProp(data)) - if err != nil { - return false, err - } - - if !m { - // No need to evaluate after we fail - break - } - } - return m, nil case time.Time: return ctime.Equal(cn, data), nil + default: return reflect.DeepEqual(condition, data), nil } } + +func immutableValueOrNil[T any](data immutable.Option[T]) any { + if data.HasValue() { + return data.Value() + } + return nil +} diff --git a/internal/connor/none.go b/internal/connor/none.go new file mode 100644 index 0000000000..16613b3e46 --- /dev/null +++ b/internal/connor/none.go @@ -0,0 +1,12 @@ +package connor + +// none is an operator which allows the evaluation of +// a number of conditions over a list of values +// matching if all of them do not match. +func none(condition, data any) (bool, error) { + m, err := anyOp(condition, data) + if err != nil { + return false, err + } + return !m, nil +} diff --git a/internal/planner/mapper/mapper.go b/internal/planner/mapper/mapper.go index 706f9235de..ac6bb80c78 100644 --- a/internal/planner/mapper/mapper.go +++ b/internal/planner/mapper/mapper.go @@ -1348,13 +1348,15 @@ func toFilterMap( returnClause := map[connor.FilterKey]any{} for innerSourceKey, innerSourceValue := range typedClause { var innerMapping *core.DocumentMapping - switch innerSourceValue.(type) { - case map[string]any: + // innerSourceValue may refer to a child mapping or + // an inline array if we don't have a child mapping + _, ok := innerSourceValue.(map[string]any) + if ok && index < len(mapping.ChildMappings) { // If the innerSourceValue is also a map, then we should parse the nested clause // using the child mapping, as this key must refer to a host property in a join // and deeper keys must refer to properties on the child items. innerMapping = mapping.ChildMappings[index] - default: + } else { innerMapping = mapping } rKey, rValue := toFilterMap(innerSourceKey, innerSourceValue, innerMapping) diff --git a/internal/request/graphql/schema/generate.go b/internal/request/graphql/schema/generate.go index c198296ffb..f5a2f4c624 100644 --- a/internal/request/graphql/schema/generate.go +++ b/internal/request/graphql/schema/generate.go @@ -1150,11 +1150,17 @@ func (g *Generator) genTypeFilterArgInput(obj *gql.Object) *gql.InputObject { } // scalars (leafs) if gql.IsLeafType(field.Type) { - if _, isList := field.Type.(*gql.List); isList { - // Filtering by inline array value is currently not supported - continue + var operatorName string + if list, isList := field.Type.(*gql.List); isList { + if notNull, isNotNull := list.OfType.(*gql.NonNull); isNotNull { + operatorName = "NotNull" + notNull.OfType.Name() + "ListOperatorBlock" + } else { + operatorName = list.OfType.Name() + "ListOperatorBlock" + } + } else { + operatorName = field.Type.Name() + "OperatorBlock" } - operatorType, isFilterable := g.manager.schema.TypeMap()[field.Type.Name()+"OperatorBlock"] + operatorType, isFilterable := g.manager.schema.TypeMap()[operatorName] if !isFilterable { continue } diff --git a/internal/request/graphql/schema/manager.go b/internal/request/graphql/schema/manager.go index 0385c50ac9..792535fda0 100644 --- a/internal/request/graphql/schema/manager.go +++ b/internal/request/graphql/schema/manager.go @@ -188,6 +188,22 @@ func defaultTypes( blobScalarType := schemaTypes.BlobScalarType() jsonScalarType := schemaTypes.JSONScalarType() + idOpBlock := schemaTypes.IDOperatorBlock() + intOpBlock := schemaTypes.IntOperatorBlock() + floatOpBlock := schemaTypes.FloatOperatorBlock() + booleanOpBlock := schemaTypes.BooleanOperatorBlock() + stringOpBlock := schemaTypes.StringOperatorBlock() + jsonOpBlock := schemaTypes.JSONOperatorBlock(jsonScalarType) + blobOpBlock := schemaTypes.BlobOperatorBlock(blobScalarType) + dateTimeOpBlock := schemaTypes.DateTimeOperatorBlock() + + notNullIntOpBlock := schemaTypes.NotNullIntOperatorBlock() + notNullFloatOpBlock := schemaTypes.NotNullFloatOperatorBlock() + notNullBooleanOpBlock := schemaTypes.NotNullBooleanOperatorBlock() + notNullStringOpBlock := schemaTypes.NotNullStringOperatorBlock() + notNullJSONOpBlock := schemaTypes.NotNullJSONOperatorBlock(jsonScalarType) + notNullBlobOpBlock := schemaTypes.NotNullBlobOperatorBlock(blobScalarType) + return []gql.Type{ // Base Scalar types gql.Boolean, @@ -207,20 +223,34 @@ func defaultTypes( orderEnum, // Filter scalar blocks - schemaTypes.BooleanOperatorBlock(), - schemaTypes.NotNullBooleanOperatorBlock(), - schemaTypes.DateTimeOperatorBlock(), - schemaTypes.FloatOperatorBlock(), - schemaTypes.NotNullFloatOperatorBlock(), - schemaTypes.IdOperatorBlock(), - schemaTypes.IntOperatorBlock(), - schemaTypes.NotNullIntOperatorBlock(), - schemaTypes.StringOperatorBlock(), - schemaTypes.NotNullstringOperatorBlock(), - schemaTypes.JSONOperatorBlock(jsonScalarType), - schemaTypes.NotNullJSONOperatorBlock(jsonScalarType), - schemaTypes.BlobOperatorBlock(blobScalarType), - schemaTypes.NotNullBlobOperatorBlock(blobScalarType), + idOpBlock, + intOpBlock, + floatOpBlock, + booleanOpBlock, + stringOpBlock, + jsonOpBlock, + blobOpBlock, + dateTimeOpBlock, + + // Filter non null scalar blocks + notNullIntOpBlock, + notNullFloatOpBlock, + notNullBooleanOpBlock, + notNullStringOpBlock, + notNullJSONOpBlock, + notNullBlobOpBlock, + + // Filter scalar list blocks + schemaTypes.IntListOperatorBlock(intOpBlock), + schemaTypes.FloatListOperatorBlock(floatOpBlock), + schemaTypes.BooleanListOperatorBlock(booleanOpBlock), + schemaTypes.StringListOperatorBlock(stringOpBlock), + + // Filter non null scalar list blocks + schemaTypes.NotNullIntListOperatorBlock(notNullIntOpBlock), + schemaTypes.NotNullFloatListOperatorBlock(notNullFloatOpBlock), + schemaTypes.NotNullBooleanListOperatorBlock(notNullBooleanOpBlock), + schemaTypes.NotNullStringListOperatorBlock(notNullStringOpBlock), commitsOrderArg, commitLinkObject, diff --git a/internal/request/graphql/schema/types/base.go b/internal/request/graphql/schema/types/base.go index fd49fbb45a..4675169989 100644 --- a/internal/request/graphql/schema/types/base.go +++ b/internal/request/graphql/schema/types/base.go @@ -40,6 +40,28 @@ func BooleanOperatorBlock() *gql.InputObject { }) } +// BooleanListOperatorBlock filter block for [Boolean] types. +func BooleanListOperatorBlock(op *gql.InputObject) *gql.InputObject { + return gql.NewInputObject(gql.InputObjectConfig{ + Name: "BooleanListOperatorBlock", + Description: "These are the set of filter operators available for use when filtering on [Boolean] values.", + Fields: gql.InputObjectConfigFieldMap{ + "_any": &gql.InputObjectFieldConfig{ + Description: anyOperatorDescription, + Type: op, + }, + "_all": &gql.InputObjectFieldConfig{ + Description: allOperatorDescription, + Type: op, + }, + "_none": &gql.InputObjectFieldConfig{ + Description: noneOperatorDescription, + Type: op, + }, + }, + }) +} + // NotNullBooleanOperatorBlock filter block for boolean! types. func NotNullBooleanOperatorBlock() *gql.InputObject { return gql.NewInputObject(gql.InputObjectConfig{ @@ -66,6 +88,28 @@ func NotNullBooleanOperatorBlock() *gql.InputObject { }) } +// NotNullBooleanListOperatorBlock filter block for [Boolean!] types. +func NotNullBooleanListOperatorBlock(op *gql.InputObject) *gql.InputObject { + return gql.NewInputObject(gql.InputObjectConfig{ + Name: "NotNullBooleanListOperatorBlock", + Description: "These are the set of filter operators available for use when filtering on [Boolean!] values.", + Fields: gql.InputObjectConfigFieldMap{ + "_any": &gql.InputObjectFieldConfig{ + Description: anyOperatorDescription, + Type: op, + }, + "_all": &gql.InputObjectFieldConfig{ + Description: allOperatorDescription, + Type: op, + }, + "_none": &gql.InputObjectFieldConfig{ + Description: noneOperatorDescription, + Type: op, + }, + }, + }) +} + // DateTimeOperatorBlock filter block for DateTime types. func DateTimeOperatorBlock() *gql.InputObject { return gql.NewInputObject(gql.InputObjectConfig{ @@ -150,6 +194,28 @@ func FloatOperatorBlock() *gql.InputObject { }) } +// FloatListOperatorBlock filter block for [Float] types. +func FloatListOperatorBlock(op *gql.InputObject) *gql.InputObject { + return gql.NewInputObject(gql.InputObjectConfig{ + Name: "FloatListOperatorBlock", + Description: "These are the set of filter operators available for use when filtering on [Float] values.", + Fields: gql.InputObjectConfigFieldMap{ + "_any": &gql.InputObjectFieldConfig{ + Description: anyOperatorDescription, + Type: op, + }, + "_all": &gql.InputObjectFieldConfig{ + Description: allOperatorDescription, + Type: op, + }, + "_none": &gql.InputObjectFieldConfig{ + Description: noneOperatorDescription, + Type: op, + }, + }, + }) +} + // NotNullFloatOperatorBlock filter block for Float! types. func NotNullFloatOperatorBlock() *gql.InputObject { return gql.NewInputObject(gql.InputObjectConfig{ @@ -192,6 +258,28 @@ func NotNullFloatOperatorBlock() *gql.InputObject { }) } +// NotNullFloatListOperatorBlock filter block for [NotNullFloat] types. +func NotNullFloatListOperatorBlock(op *gql.InputObject) *gql.InputObject { + return gql.NewInputObject(gql.InputObjectConfig{ + Name: "NotNullFloatListOperatorBlock", + Description: "These are the set of filter operators available for use when filtering on [Float!] values.", + Fields: gql.InputObjectConfigFieldMap{ + "_any": &gql.InputObjectFieldConfig{ + Description: anyOperatorDescription, + Type: op, + }, + "_all": &gql.InputObjectFieldConfig{ + Description: allOperatorDescription, + Type: op, + }, + "_none": &gql.InputObjectFieldConfig{ + Description: noneOperatorDescription, + Type: op, + }, + }, + }) +} + // IntOperatorBlock filter block for Int types. func IntOperatorBlock() *gql.InputObject { return gql.NewInputObject(gql.InputObjectConfig{ @@ -234,6 +322,28 @@ func IntOperatorBlock() *gql.InputObject { }) } +// IntListOperatorBlock filter block for [Int] types. +func IntListOperatorBlock(op *gql.InputObject) *gql.InputObject { + return gql.NewInputObject(gql.InputObjectConfig{ + Name: "IntListOperatorBlock", + Description: "These are the set of filter operators available for use when filtering on [Int] values.", + Fields: gql.InputObjectConfigFieldMap{ + "_any": &gql.InputObjectFieldConfig{ + Description: anyOperatorDescription, + Type: op, + }, + "_all": &gql.InputObjectFieldConfig{ + Description: allOperatorDescription, + Type: op, + }, + "_none": &gql.InputObjectFieldConfig{ + Description: noneOperatorDescription, + Type: op, + }, + }, + }) +} + // NotNullIntOperatorBlock filter block for Int! types. func NotNullIntOperatorBlock() *gql.InputObject { return gql.NewInputObject(gql.InputObjectConfig{ @@ -276,6 +386,28 @@ func NotNullIntOperatorBlock() *gql.InputObject { }) } +// NotNullIntListOperatorBlock filter block for [NotNullInt] types. +func NotNullIntListOperatorBlock(op *gql.InputObject) *gql.InputObject { + return gql.NewInputObject(gql.InputObjectConfig{ + Name: "NotNullIntListOperatorBlock", + Description: "These are the set of filter operators available for use when filtering on [Int!] values.", + Fields: gql.InputObjectConfigFieldMap{ + "_any": &gql.InputObjectFieldConfig{ + Description: anyOperatorDescription, + Type: op, + }, + "_all": &gql.InputObjectFieldConfig{ + Description: allOperatorDescription, + Type: op, + }, + "_none": &gql.InputObjectFieldConfig{ + Description: noneOperatorDescription, + Type: op, + }, + }, + }) +} + // StringOperatorBlock filter block for string types. func StringOperatorBlock() *gql.InputObject { return gql.NewInputObject(gql.InputObjectConfig{ @@ -318,8 +450,30 @@ func StringOperatorBlock() *gql.InputObject { }) } -// NotNullstringOperatorBlock filter block for string! types. -func NotNullstringOperatorBlock() *gql.InputObject { +// StringListOperatorBlock filter block for [String] types. +func StringListOperatorBlock(op *gql.InputObject) *gql.InputObject { + return gql.NewInputObject(gql.InputObjectConfig{ + Name: "StringListOperatorBlock", + Description: "These are the set of filter operators available for use when filtering on [String] values.", + Fields: gql.InputObjectConfigFieldMap{ + "_any": &gql.InputObjectFieldConfig{ + Description: anyOperatorDescription, + Type: op, + }, + "_all": &gql.InputObjectFieldConfig{ + Description: allOperatorDescription, + Type: op, + }, + "_none": &gql.InputObjectFieldConfig{ + Description: noneOperatorDescription, + Type: op, + }, + }, + }) +} + +// NotNullStringOperatorBlock filter block for string! types. +func NotNullStringOperatorBlock() *gql.InputObject { return gql.NewInputObject(gql.InputObjectConfig{ Name: "NotNullStringOperatorBlock", Description: notNullStringOperatorBlockDescription, @@ -360,6 +514,28 @@ func NotNullstringOperatorBlock() *gql.InputObject { }) } +// NotNullStringListOperatorBlock filter block for [String!] types. +func NotNullStringListOperatorBlock(op *gql.InputObject) *gql.InputObject { + return gql.NewInputObject(gql.InputObjectConfig{ + Name: "NotNullStringListOperatorBlock", + Description: "These are the set of filter operators available for use when filtering on [String!] values.", + Fields: gql.InputObjectConfigFieldMap{ + "_any": &gql.InputObjectFieldConfig{ + Description: anyOperatorDescription, + Type: op, + }, + "_all": &gql.InputObjectFieldConfig{ + Description: allOperatorDescription, + Type: op, + }, + "_none": &gql.InputObjectFieldConfig{ + Description: noneOperatorDescription, + Type: op, + }, + }, + }) +} + // JSONOperatorBlock filter block for string types. func JSONOperatorBlock(jsonScalarType *gql.Scalar) *gql.InputObject { return gql.NewInputObject(gql.InputObjectConfig{ @@ -527,8 +703,8 @@ func NotNullBlobOperatorBlock(blobScalarType *gql.Scalar) *gql.InputObject { }) } -// IdOperatorBlock filter block for ID types. -func IdOperatorBlock() *gql.InputObject { +// IDOperatorBlock filter block for ID types. +func IDOperatorBlock() *gql.InputObject { return gql.NewInputObject(gql.InputObjectConfig{ Name: "IDOperatorBlock", Description: idOperatorBlockDescription, diff --git a/internal/request/graphql/schema/types/descriptions.go b/internal/request/graphql/schema/types/descriptions.go index 27cd3a6f74..213266d891 100644 --- a/internal/request/graphql/schema/types/descriptions.go +++ b/internal/request/graphql/schema/types/descriptions.go @@ -222,6 +222,16 @@ The or operator - only one check within this clause must pass in order for this NotOperatorDescription string = ` The negative operator - this check will only pass if all checks within it fail. ` + anyOperatorDescription string = ` +The any operator - only one check within this clause must pass on each item in order for this check to pass. +` + allOperatorDescription string = ` +The all operator - all checks within this clause must pass on each item in order for this check to pass. +` + noneOperatorDescription string = ` +The none operator - only one check within this clause must fail on one item in order for this check to pass. +` + ascOrderDescription string = ` Sort the results in ascending order, e.g. null,1,2,3,a,b,c. ` diff --git a/tests/integration/query/inline_array/with_filter_all_test.go b/tests/integration/query/inline_array/with_filter_all_test.go new file mode 100644 index 0000000000..1661c54731 --- /dev/null +++ b/tests/integration/query/inline_array/with_filter_all_test.go @@ -0,0 +1,305 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package inline_array + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryInlineStringArray_WithAllFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered all of string array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "pageHeaders": ["first", "second"] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "pageHeaders": [null, "second"] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {pageHeaders: {_all: {_ne: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNotNullStringArray_WithAllFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered all of non null string array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "preferredStrings": ["first", "second"] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "preferredStrings": ["", "second"] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {preferredStrings: {_all: {_ne: ""}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineIntArray_WithAllFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered all of int array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "testScores": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "testScores": [null, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {testScores: {_all: {_ne: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNotNullIntArray_WithAllFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered all of non null int array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "testScores": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "testScores": [0, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {testScores: {_all: {_lt: 70}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineFloatArray_WithAllFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered all of float array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "pageRatings": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "pageRatings": [null, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {pageRatings: {_all: {_ne: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNotNullFloatArray_WithAllFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered all of non null float array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "pageRatings": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "pageRatings": [0, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {pageRatings: {_all: {_lt: 70}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineBooleanArray_WithAllFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered all of boolean array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "indexLikesDislikes": [false, false] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "indexLikesDislikes": [null, true] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {indexLikesDislikes: {_all: {_ne: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNotNullBooleanArray_WithAllFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered all of non null boolean array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "likedIndexes": [false, false] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "likedIndexes": [true, true] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {likedIndexes: {_all: {_eq: true}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/query/inline_array/with_filter_any_test.go b/tests/integration/query/inline_array/with_filter_any_test.go new file mode 100644 index 0000000000..0dfc815595 --- /dev/null +++ b/tests/integration/query/inline_array/with_filter_any_test.go @@ -0,0 +1,305 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package inline_array + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryInlineStringArray_WithAnyFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered any of string array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "pageHeaders": ["first", "second"] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "pageHeaders": [null, "second"] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {pageHeaders: {_any: {_eq: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNotNullStringArray_WithAnyFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered any of non null string array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "preferredStrings": ["first", "second"] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "preferredStrings": ["", "second"] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {preferredStrings: {_any: {_eq: ""}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineIntArray_WithAnyFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered any of int array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "testScores": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "testScores": [null, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {testScores: {_any: {_eq: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNotNullIntArray_WithAnyFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered any of non null int array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "testScores": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "testScores": [0, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {testScores: {_any: {_gt: 70}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineFloatArray_WithAnyFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered any of float array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "pageRatings": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "pageRatings": [null, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {pageRatings: {_any: {_eq: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNotNullFloatArray_WithAnyFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered any of non null float array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "pageRatings": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "pageRatings": [0, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {pageRatings: {_any: {_gt: 70}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineBooleanArray_WithAnyFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered any of boolean array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "indexLikesDislikes": [false, false] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "indexLikesDislikes": [null, true] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {indexLikesDislikes: {_any: {_eq: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNotNullBooleanArray_WithAnyFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered any of non null boolean array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "likedIndexes": [false, false] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "likedIndexes": [true, true] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {likedIndexes: {_any: {_eq: true}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/query/inline_array/with_filter_none_test.go b/tests/integration/query/inline_array/with_filter_none_test.go new file mode 100644 index 0000000000..0dcb45b4f7 --- /dev/null +++ b/tests/integration/query/inline_array/with_filter_none_test.go @@ -0,0 +1,305 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package inline_array + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryInlineStringArrayWithNoneFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered none of string array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "pageHeaders": ["first", "second"] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "pageHeaders": [null, "second"] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {pageHeaders: {_none: {_eq: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNonNullStringArrayWithNoneFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered none of non null string array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "preferredStrings": ["first", "second"] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "preferredStrings": ["", "second"] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {preferredStrings: {_none: {_eq: ""}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineIntArrayWithNoneFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered none of int array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "testScores": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "testScores": [null, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {testScores: {_none: {_eq: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNonNullIntArrayWithNoneFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered none of non null int array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "testScores": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "testScores": [0, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {testScores: {_none: {_gt: 70}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineFloatArrayWithNoneFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered none of float array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "pageRatings": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "pageRatings": [null, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {pageRatings: {_none: {_eq: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNonNullFloatArrayWithNoneFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered none of non null float array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "pageRatings": [50, 80] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "pageRatings": [0, 60] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {pageRatings: {_none: {_gt: 70}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineBooleanArrayWithNoneFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered none of boolean array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "indexLikesDislikes": [false, false] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "indexLikesDislikes": [null, true] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {indexLikesDislikes: {_none: {_eq: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineNonNullBooleanArrayWithNoneFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple inline array, filtered none of non null boolean array", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "likedIndexes": [false, false] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "likedIndexes": [true, true] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {likedIndexes: {_none: {_ne: true}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/query/simple/with_filter/with_and_test.go b/tests/integration/query/simple/with_filter/with_and_test.go index 81ccbeb35f..eb566fd751 100644 --- a/tests/integration/query/simple/with_filter/with_and_test.go +++ b/tests/integration/query/simple/with_filter/with_and_test.go @@ -69,3 +69,48 @@ func TestQuerySimpleWithIntGreaterThanAndIntLessThanFilter(t *testing.T) { executeTestCase(t, test) } + +func TestQuerySimple_WithInlineIntArray_GreaterThanAndLessThanFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with logical compound filter (and) on inline int array", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: `type Users { + Name: String + FavoriteNumbers: [Int!] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "FavoriteNumbers": [0, 10, 20] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "FavoriteNumbers": [30, 40, 50] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {_and: [ + {FavoriteNumbers: {_all: {_ge: 0}}}, + {FavoriteNumbers: {_all: {_lt: 30}}}, + ]}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "Bob", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/simple/with_filter/with_or_test.go b/tests/integration/query/simple/with_filter/with_or_test.go index fe63d7cc62..e208693049 100644 --- a/tests/integration/query/simple/with_filter/with_or_test.go +++ b/tests/integration/query/simple/with_filter/with_or_test.go @@ -69,3 +69,51 @@ func TestQuerySimpleWithIntEqualToXOrYFilter(t *testing.T) { executeTestCase(t, test) } + +func TestQuerySimple_WithInlineIntArray_EqualToXOrYFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with logical compound filter (or) on inline int array", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: `type Users { + Name: String + FavoriteNumbers: [Int!] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "FavoriteNumbers": [10, 20] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "FavoriteNumbers": [30, 40] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {_or: [ + {FavoriteNumbers: {_any: {_le: 100}}}, + {FavoriteNumbers: {_any: {_ge: 0}}}, + ]}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "Alice", + }, + { + "Name": "Bob", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/schema/aggregates/inline_array_test.go b/tests/integration/schema/aggregates/inline_array_test.go index 1dfaa4a858..a7fa6518fa 100644 --- a/tests/integration/schema/aggregates/inline_array_test.go +++ b/tests/integration/schema/aggregates/inline_array_test.go @@ -388,59 +388,67 @@ func TestSchemaAggregateInlineArrayCreatesUsersAverage(t *testing.T) { } */ -var aggregateGroupArg = map[string]any{ - "name": "_group", - "type": map[string]any{ - "name": "Users__CountSelector", - "inputFields": []any{ - map[string]any{ - "name": "filter", - "type": map[string]any{ - "name": "UsersFilterArg", - "inputFields": []any{ - map[string]any{ - "name": "_and", - "type": map[string]any{ - "name": nil, +func aggregateGroupArg(fieldType string) map[string]any { + return map[string]any{ + "name": "_group", + "type": map[string]any{ + "name": "Users__CountSelector", + "inputFields": []any{ + map[string]any{ + "name": "filter", + "type": map[string]any{ + "name": "UsersFilterArg", + "inputFields": []any{ + map[string]any{ + "name": "Favourites", + "type": map[string]any{ + "name": fieldType + "ListOperatorBlock", + }, }, - }, - map[string]any{ - "name": "_docID", - "type": map[string]any{ - "name": "IDOperatorBlock", + map[string]any{ + "name": "_and", + "type": map[string]any{ + "name": nil, + }, }, - }, - map[string]any{ - "name": "_not", - "type": map[string]any{ - "name": "UsersFilterArg", + map[string]any{ + "name": "_docID", + "type": map[string]any{ + "name": "IDOperatorBlock", + }, }, - }, - map[string]any{ - "name": "_or", - "type": map[string]any{ - "name": nil, + map[string]any{ + "name": "_not", + "type": map[string]any{ + "name": "UsersFilterArg", + }, + }, + map[string]any{ + "name": "_or", + "type": map[string]any{ + "name": nil, + }, }, }, }, }, - }, - map[string]any{ - "name": "limit", - "type": map[string]any{ - "name": "Int", - "inputFields": nil, + map[string]any{ + "name": "limit", + "type": map[string]any{ + "name": "Int", + "inputFields": nil, + }, }, - }, - map[string]any{ - "name": "offset", - "type": map[string]any{ - "name": "Int", - "inputFields": nil, + map[string]any{ + "name": "offset", + "type": map[string]any{ + "name": "Int", + "inputFields": nil, + }, }, }, }, - }, + } } var aggregateVersionArg = map[string]any{ @@ -578,7 +586,7 @@ func TestSchemaAggregateInlineArrayCreatesUsersNillableBooleanCountFilter(t *tes }, }, }, - aggregateGroupArg, + aggregateGroupArg("Boolean"), aggregateVersionArg, }, }, @@ -704,7 +712,7 @@ func TestSchemaAggregateInlineArrayCreatesUsersBooleanCountFilter(t *testing.T) }, }, }, - aggregateGroupArg, + aggregateGroupArg("NotNullBoolean"), aggregateVersionArg, }, }, @@ -854,7 +862,7 @@ func TestSchemaAggregateInlineArrayCreatesUsersNillableIntegerCountFilter(t *tes }, }, }, - aggregateGroupArg, + aggregateGroupArg("Int"), aggregateVersionArg, }, }, @@ -1004,7 +1012,7 @@ func TestSchemaAggregateInlineArrayCreatesUsersIntegerCountFilter(t *testing.T) }, }, }, - aggregateGroupArg, + aggregateGroupArg("NotNullInt"), aggregateVersionArg, }, }, @@ -1154,7 +1162,7 @@ func TestSchemaAggregateInlineArrayCreatesUsersNillableFloatCountFilter(t *testi }, }, }, - aggregateGroupArg, + aggregateGroupArg("Float"), aggregateVersionArg, }, }, @@ -1304,7 +1312,7 @@ func TestSchemaAggregateInlineArrayCreatesUsersFloatCountFilter(t *testing.T) { }, }, }, - aggregateGroupArg, + aggregateGroupArg("NotNullFloat"), aggregateVersionArg, }, }, @@ -1454,7 +1462,7 @@ func TestSchemaAggregateInlineArrayCreatesUsersNillableStringCountFilter(t *test }, }, }, - aggregateGroupArg, + aggregateGroupArg("String"), aggregateVersionArg, }, }, @@ -1604,7 +1612,7 @@ func TestSchemaAggregateInlineArrayCreatesUsersStringCountFilter(t *testing.T) { }, }, }, - aggregateGroupArg, + aggregateGroupArg("NotNullString"), aggregateVersionArg, }, },