From aa6f235d2c920ef29f3754318440b3bd961c690b Mon Sep 17 00:00:00 2001 From: Keenan Nemetz Date: Fri, 11 Oct 2024 13:40:34 -0700 Subject: [PATCH] feat: JSON type filter (#3122) ## Relevant issue(s) Resolves #3106 ## Description This PR enables filtering on JSON field types. ## Tasks - [x] I made sure the code is well commented, particularly hard-to-understand areas. - [x] I made sure the repository-held documentation is changed accordingly. - [x] I made sure the pull request title adheres to the conventional commit style (the subset used in the project can be found in [tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)). - [x] I made sure to discuss its limitations such as threats to validity, vulnerability to mistake and misuse, robustness to invalidation of assumptions, resource requirements, ... ## How has this been tested? Added integration tests. Specify the platform(s) on which this was tested: - MacOS --- internal/connor/all.go | 3 + internal/connor/any.go | 3 + internal/planner/mapper/mapper.go | 139 ++--- internal/planner/mapper/targetable.go | 37 ++ internal/request/graphql/schema/generate.go | 72 +-- internal/request/graphql/schema/manager.go | 4 - internal/request/graphql/schema/types/base.go | 84 --- .../explain/default/with_filter_test.go | 49 ++ tests/integration/query/json/with_all_test.go | 59 +++ tests/integration/query/json/with_any_test.go | 59 +++ .../with_eq_test.go} | 8 +- tests/integration/query/json/with_ge_test.go | 499 ++++++++++++++++++ tests/integration/query/json/with_gt_test.go | 497 +++++++++++++++++ .../with_in_test.go} | 4 +- tests/integration/query/json/with_le_test.go | 493 +++++++++++++++++ .../integration/query/json/with_like_test.go | 77 +++ tests/integration/query/json/with_lt_test.go | 489 +++++++++++++++++ tests/integration/query/json/with_ne_test.go | 165 ++++++ .../with_nin_test.go} | 30 +- .../integration/query/json/with_nlike_test.go | 89 ++++ .../integration/query/json/with_none_test.go | 59 +++ tests/integration/schema/filter_test.go | 123 +++++ tests/integration/schema/simple_test.go | 46 ++ 23 files changed, 2886 insertions(+), 202 deletions(-) create mode 100644 tests/integration/query/json/with_all_test.go create mode 100644 tests/integration/query/json/with_any_test.go rename tests/integration/query/{simple/with_filter/with_eq_json_test.go => json/with_eq_test.go} (91%) create mode 100644 tests/integration/query/json/with_ge_test.go create mode 100644 tests/integration/query/json/with_gt_test.go rename tests/integration/query/{simple/with_filter/with_in_json_test.go => json/with_in_test.go} (93%) create mode 100644 tests/integration/query/json/with_le_test.go create mode 100644 tests/integration/query/json/with_like_test.go create mode 100644 tests/integration/query/json/with_lt_test.go create mode 100644 tests/integration/query/json/with_ne_test.go rename tests/integration/query/{simple/with_filter/with_like_json_test.go => json/with_nin_test.go} (70%) create mode 100644 tests/integration/query/json/with_nlike_test.go create mode 100644 tests/integration/query/json/with_none_test.go diff --git a/internal/connor/all.go b/internal/connor/all.go index 0b9800de89..ce2557d25b 100644 --- a/internal/connor/all.go +++ b/internal/connor/all.go @@ -11,6 +11,9 @@ import ( // matching if all of them match. func all(condition, data any) (bool, error) { switch t := data.(type) { + case []any: + return allSlice(condition, t) + case []string: return allSlice(condition, t) diff --git a/internal/connor/any.go b/internal/connor/any.go index a9c02b1369..7eea2a7bce 100644 --- a/internal/connor/any.go +++ b/internal/connor/any.go @@ -11,6 +11,9 @@ import ( // matching if any of them match. func anyOp(condition, data any) (bool, error) { switch t := data.(type) { + case []any: + return anySlice(condition, t) + case []string: return anySlice(condition, t) diff --git a/internal/planner/mapper/mapper.go b/internal/planner/mapper/mapper.go index da8390e293..9845e93d13 100644 --- a/internal/planner/mapper/mapper.go +++ b/internal/planner/mapper/mapper.go @@ -1286,7 +1286,7 @@ func ToFilter(source request.Filter, mapping *core.DocumentMapping) *Filter { conditions := make(map[connor.FilterKey]any, len(source.Conditions)) for sourceKey, sourceClause := range source.Conditions { - key, clause := toFilterMap(sourceKey, sourceClause, mapping) + key, clause := toFilterKeyValue(sourceKey, sourceClause, mapping) conditions[key] = clause } @@ -1296,87 +1296,102 @@ func ToFilter(source request.Filter, mapping *core.DocumentMapping) *Filter { } } -// toFilterMap converts a consumer-defined filter key-value into a filter clause -// keyed by field index. +// toFilterKeyValue converts a consumer-defined filter key-value into a filter clause +// keyed by connor.FilterKey. // -// Return key will either be an int (field index), or a string (operator). -func toFilterMap( +// The returned key will be one of the following: +// - Operator: if the sourceKey is one of the defined filter operators +// - PropertyIndex: if the sourceKey exists in the document mapping +// - ObjectProperty: if the sourceKey does not match one of the above +func toFilterKeyValue( sourceKey string, sourceClause any, mapping *core.DocumentMapping, ) (connor.FilterKey, any) { + var returnKey connor.FilterKey if strings.HasPrefix(sourceKey, "_") && sourceKey != request.DocIDFieldName { - key := &Operator{ + returnKey = &Operator{ Operation: sourceKey, } // if the operator is simple (not compound) then // it does not require further expansion if connor.IsOpSimple(sourceKey) { - return key, sourceClause - } - switch typedClause := sourceClause.(type) { - case []any: - // If the clause is an array then we need to convert any inner maps. - returnClauses := []any{} - for _, innerSourceClause := range typedClause { - var returnClause any - switch typedInnerSourceClause := innerSourceClause.(type) { - case map[string]any: - innerMapClause := map[connor.FilterKey]any{} - for innerSourceKey, innerSourceValue := range typedInnerSourceClause { - rKey, rValue := toFilterMap(innerSourceKey, innerSourceValue, mapping) - innerMapClause[rKey] = rValue - } - returnClause = innerMapClause - default: - returnClause = innerSourceClause - } - returnClauses = append(returnClauses, returnClause) - } - return key, returnClauses - case map[string]any: - innerMapClause := map[connor.FilterKey]any{} - for innerSourceKey, innerSourceValue := range typedClause { - rKey, rValue := toFilterMap(innerSourceKey, innerSourceValue, mapping) - innerMapClause[rKey] = rValue - } - return key, innerMapClause - default: - return key, typedClause + return returnKey, sourceClause } - } else { + } else if mapping != nil && len(mapping.IndexesByName[sourceKey]) > 0 { // If there are multiple properties of the same name we can just take the first as // we have no other reasonable way of identifying which property they mean if multiple // consumer specified requestables are available. Aggregate dependencies should not // impact this as they are added after selects. - index := mapping.FirstIndexOfName(sourceKey) - key := &PropertyIndex{ - Index: index, + returnKey = &PropertyIndex{ + Index: mapping.FirstIndexOfName(sourceKey), } - switch typedClause := sourceClause.(type) { - case map[string]any: - returnClause := map[connor.FilterKey]any{} - for innerSourceKey, innerSourceValue := range typedClause { - var innerMapping *core.DocumentMapping - // 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] - } else { - innerMapping = mapping - } - rKey, rValue := toFilterMap(innerSourceKey, innerSourceValue, innerMapping) - returnClause[rKey] = rValue + } else { + returnKey = &ObjectProperty{ + Name: sourceKey, + } + } + + switch typedClause := sourceClause.(type) { + case []any: + return returnKey, toFilterList(typedClause, mapping) + + case map[string]any: + return returnKey, toFilterMap(returnKey, typedClause, mapping) + + default: + return returnKey, typedClause + } +} + +func toFilterMap( + sourceKey connor.FilterKey, + sourceClause map[string]any, + mapping *core.DocumentMapping, +) map[connor.FilterKey]any { + innerMapClause := make(map[connor.FilterKey]any) + for innerSourceKey, innerSourceValue := range sourceClause { + var innerMapping *core.DocumentMapping + switch t := sourceKey.(type) { + case *PropertyIndex: + _, ok := innerSourceValue.(map[string]any) + if ok && mapping != nil && t.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[t.Index] + } else { + innerMapping = mapping } - return key, returnClause - default: - return key, sourceClause + case *ObjectProperty: + // Object properties can never refer to mapped document fields. + // Set the mapping to null for any nested filter values so + // that we don't filter any fields outside of this object. + innerMapping = nil + case *Operator: + innerMapping = mapping + } + rKey, rValue := toFilterKeyValue(innerSourceKey, innerSourceValue, innerMapping) + innerMapClause[rKey] = rValue + } + return innerMapClause +} + +func toFilterList(sourceClause []any, mapping *core.DocumentMapping) []any { + returnClauses := make([]any, len(sourceClause)) + for i, innerSourceClause := range sourceClause { + // innerSourceClause must be a map because only compound + // operators (_and, _or) can reach this function and should + // have already passed GQL type validation + typedInnerSourceClause := innerSourceClause.(map[string]any) + innerMapClause := make(map[connor.FilterKey]any) + for innerSourceKey, innerSourceValue := range typedInnerSourceClause { + rKey, rValue := toFilterKeyValue(innerSourceKey, innerSourceValue, mapping) + innerMapClause[rKey] = rValue } + returnClauses[i] = innerMapClause } + return returnClauses } func toLimit(limit immutable.Option[uint64], offset immutable.Option[uint64]) *Limit { diff --git a/internal/planner/mapper/targetable.go b/internal/planner/mapper/targetable.go index f85e6c8016..2611d297dc 100644 --- a/internal/planner/mapper/targetable.go +++ b/internal/planner/mapper/targetable.go @@ -21,6 +21,7 @@ import ( var ( _ connor.FilterKey = (*PropertyIndex)(nil) _ connor.FilterKey = (*Operator)(nil) + _ connor.FilterKey = (*ObjectProperty)(nil) ) // PropertyIndex is a FilterKey that represents a property in a document. @@ -71,6 +72,34 @@ func (k *Operator) Equal(other connor.FilterKey) bool { return false } +// ObjectProperty is a FilterKey that represents a property in an object. +// +// This is used to target properties of an object when the fields +// are not explicitly mapped, such as with JSON. +type ObjectProperty struct { + // The name of the property on object. + Name string +} + +func (k *ObjectProperty) GetProp(data any) any { + if data == nil { + return nil + } + object := data.(map[string]any) + return object[k.Name] +} + +func (k *ObjectProperty) GetOperatorOrDefault(defaultOp string) string { + return defaultOp +} + +func (k *ObjectProperty) Equal(other connor.FilterKey) bool { + if otherKey, isOk := other.(*ObjectProperty); isOk && *k == *otherKey { + return true + } + return false +} + // Filter represents a series of conditions that may reduce the number of // records that a request returns. type Filter struct { @@ -144,6 +173,14 @@ func filterObjectToMap(mapping *core.DocumentMapping, obj map[connor.FilterKey]a default: outmap[keyType.Operation] = v } + + case *ObjectProperty: + switch subObj := v.(type) { + case map[connor.FilterKey]any: + outmap[keyType.Name] = filterObjectToMap(mapping, subObj) + case nil: + outmap[keyType.Name] = nil + } } } return outmap diff --git a/internal/request/graphql/schema/generate.go b/internal/request/graphql/schema/generate.go index c1cf92e4dc..254fae6e7d 100644 --- a/internal/request/graphql/schema/generate.go +++ b/internal/request/graphql/schema/generate.go @@ -1202,43 +1202,18 @@ func (g *Generator) genTypeFilterArgInput(obj *gql.Object) *gql.InputObject { } // generate basic filter operator blocks - // @todo: Extract object field loop into its own utility func for f, field := range obj.Fields() { - if _, ok := request.ReservedFields[f]; ok && f != request.DocIDFieldName { + _, ok := request.ReservedFields[f] + if ok && f != request.DocIDFieldName { continue } - // scalars (leafs) - if gql.IsLeafType(field.Type) { - 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()[operatorName] - if !isFilterable { - continue - } - fields[field.Name] = &gql.InputObjectFieldConfig{ - Type: operatorType, - } - } else { // objects (relations) - fieldType := field.Type - if l, isList := field.Type.(*gql.List); isList { - // We want the FilterArg for the object, not the list of objects. - fieldType = l.OfType - } - filterType, isFilterable := g.manager.schema.TypeMap()[genTypeName(fieldType, filterInputNameSuffix)] - if !isFilterable { - filterType = &gql.InputObjectField{} - } - fields[field.Name] = &gql.InputObjectFieldConfig{ - Type: filterType, - } + operatorName := genFilterOperatorName(field.Type) + filterType, isFilterable := g.manager.schema.TypeMap()[operatorName] + if !isFilterable { + continue + } + fields[field.Name] = &gql.InputObjectFieldConfig{ + Type: filterType, } } @@ -1408,6 +1383,35 @@ func isNumericArray(list *gql.List) bool { list.OfType == gql.Float } +func genFilterOperatorName(fieldType gql.Type) string { + list, isList := fieldType.(*gql.List) + if isList { + fieldType = list.OfType + } + if !gql.IsLeafType(fieldType) { + return genTypeName(fieldType, filterInputNameSuffix) + } + notNull, isNotNull := fieldType.(*gql.NonNull) + if isNotNull { + fieldType = notNull.OfType + } + switch { + case fieldType.Name() == "JSON": + return fieldType.Name() + + case isList && isNotNull: + // todo: There's a potential to have a name clash + // https://github.com/sourcenetwork/defradb/issues/3123 + return "NotNull" + fieldType.Name() + "ListOperatorBlock" + + case isList: + return fieldType.Name() + "ListOperatorBlock" + + default: + return fieldType.Name() + "OperatorBlock" + } +} + /* Example typeDefs := ` ... ` diff --git a/internal/request/graphql/schema/manager.go b/internal/request/graphql/schema/manager.go index 792535fda0..881c94c144 100644 --- a/internal/request/graphql/schema/manager.go +++ b/internal/request/graphql/schema/manager.go @@ -193,7 +193,6 @@ func defaultTypes( floatOpBlock := schemaTypes.FloatOperatorBlock() booleanOpBlock := schemaTypes.BooleanOperatorBlock() stringOpBlock := schemaTypes.StringOperatorBlock() - jsonOpBlock := schemaTypes.JSONOperatorBlock(jsonScalarType) blobOpBlock := schemaTypes.BlobOperatorBlock(blobScalarType) dateTimeOpBlock := schemaTypes.DateTimeOperatorBlock() @@ -201,7 +200,6 @@ func defaultTypes( notNullFloatOpBlock := schemaTypes.NotNullFloatOperatorBlock() notNullBooleanOpBlock := schemaTypes.NotNullBooleanOperatorBlock() notNullStringOpBlock := schemaTypes.NotNullStringOperatorBlock() - notNullJSONOpBlock := schemaTypes.NotNullJSONOperatorBlock(jsonScalarType) notNullBlobOpBlock := schemaTypes.NotNullBlobOperatorBlock(blobScalarType) return []gql.Type{ @@ -228,7 +226,6 @@ func defaultTypes( floatOpBlock, booleanOpBlock, stringOpBlock, - jsonOpBlock, blobOpBlock, dateTimeOpBlock, @@ -237,7 +234,6 @@ func defaultTypes( notNullFloatOpBlock, notNullBooleanOpBlock, notNullStringOpBlock, - notNullJSONOpBlock, notNullBlobOpBlock, // Filter scalar list blocks diff --git a/internal/request/graphql/schema/types/base.go b/internal/request/graphql/schema/types/base.go index 4675169989..8dc3b35717 100644 --- a/internal/request/graphql/schema/types/base.go +++ b/internal/request/graphql/schema/types/base.go @@ -536,90 +536,6 @@ func NotNullStringListOperatorBlock(op *gql.InputObject) *gql.InputObject { }) } -// JSONOperatorBlock filter block for string types. -func JSONOperatorBlock(jsonScalarType *gql.Scalar) *gql.InputObject { - return gql.NewInputObject(gql.InputObjectConfig{ - Name: "JSONOperatorBlock", - Description: stringOperatorBlockDescription, - Fields: gql.InputObjectConfigFieldMap{ - "_eq": &gql.InputObjectFieldConfig{ - Description: eqOperatorDescription, - Type: jsonScalarType, - }, - "_ne": &gql.InputObjectFieldConfig{ - Description: neOperatorDescription, - Type: jsonScalarType, - }, - "_in": &gql.InputObjectFieldConfig{ - Description: inOperatorDescription, - Type: gql.NewList(jsonScalarType), - }, - "_nin": &gql.InputObjectFieldConfig{ - Description: ninOperatorDescription, - Type: gql.NewList(jsonScalarType), - }, - "_like": &gql.InputObjectFieldConfig{ - Description: likeStringOperatorDescription, - Type: gql.String, - }, - "_nlike": &gql.InputObjectFieldConfig{ - Description: nlikeStringOperatorDescription, - Type: gql.String, - }, - "_ilike": &gql.InputObjectFieldConfig{ - Description: ilikeStringOperatorDescription, - Type: gql.String, - }, - "_nilike": &gql.InputObjectFieldConfig{ - Description: nilikeStringOperatorDescription, - Type: gql.String, - }, - }, - }) -} - -// NotNullJSONOperatorBlock filter block for string! types. -func NotNullJSONOperatorBlock(jsonScalarType *gql.Scalar) *gql.InputObject { - return gql.NewInputObject(gql.InputObjectConfig{ - Name: "NotNullJSONOperatorBlock", - Description: notNullStringOperatorBlockDescription, - Fields: gql.InputObjectConfigFieldMap{ - "_eq": &gql.InputObjectFieldConfig{ - Description: eqOperatorDescription, - Type: jsonScalarType, - }, - "_ne": &gql.InputObjectFieldConfig{ - Description: neOperatorDescription, - Type: jsonScalarType, - }, - "_in": &gql.InputObjectFieldConfig{ - Description: inOperatorDescription, - Type: gql.NewList(gql.NewNonNull(jsonScalarType)), - }, - "_nin": &gql.InputObjectFieldConfig{ - Description: ninOperatorDescription, - Type: gql.NewList(gql.NewNonNull(jsonScalarType)), - }, - "_like": &gql.InputObjectFieldConfig{ - Description: likeStringOperatorDescription, - Type: gql.String, - }, - "_nlike": &gql.InputObjectFieldConfig{ - Description: nlikeStringOperatorDescription, - Type: gql.String, - }, - "_ilike": &gql.InputObjectFieldConfig{ - Description: ilikeStringOperatorDescription, - Type: gql.String, - }, - "_nilike": &gql.InputObjectFieldConfig{ - Description: nilikeStringOperatorDescription, - Type: gql.String, - }, - }, - }) -} - func BlobOperatorBlock(blobScalarType *gql.Scalar) *gql.InputObject { return gql.NewInputObject(gql.InputObjectConfig{ Name: "BlobOperatorBlock", diff --git a/tests/integration/explain/default/with_filter_test.go b/tests/integration/explain/default/with_filter_test.go index 2d3751f562..96e99e19ac 100644 --- a/tests/integration/explain/default/with_filter_test.go +++ b/tests/integration/explain/default/with_filter_test.go @@ -320,3 +320,52 @@ func TestDefaultExplainRequestWithMatchInsideList(t *testing.T) { explainUtils.ExecuteTestCase(t, test) } + +func TestDefaultExplainRequest_WithJSONEqualFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Explain (default) request with JSON equal (_eq) filter.", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: `type Users { + name: String + custom: JSON + }`, + }, + testUtils.ExplainRequest{ + Request: `query @explain { + Users(filter: {custom: {_eq: {one: {two: 3}}}}) { + name + } + }`, + ExpectedPatterns: basicPattern, + ExpectedTargets: []testUtils.PlanNodeTargetCase{ + { + TargetNodeName: "scanNode", + IncludeChildNodes: true, // should be last node, so will have no child nodes. + ExpectedAttributes: dataMap{ + "collectionID": "1", + "collectionName": "Users", + "filter": dataMap{ + "custom": dataMap{ + "_eq": dataMap{ + "one": dataMap{ + "two": int32(3), + }, + }, + }, + }, + "spans": []dataMap{ + { + "start": "/1", + "end": "/2", + }, + }, + }, + }, + }, + }, + }, + } + + explainUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/json/with_all_test.go b/tests/integration/query/json/with_all_test.go new file mode 100644 index 0000000000..918c51e5bb --- /dev/null +++ b/tests/integration/query/json/with_all_test.go @@ -0,0 +1,59 @@ +// 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 json + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryJSON_WithAllFilter_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple JSON array, filtered all of string array", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: `type Users { + name: String + custom: JSON + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "custom": [1, false, "second", {"one": 1}, [1, 2]] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "custom": [null, false, "second", {"one": 1}, [1, 2]] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {custom: {_all: {_ne: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/json/with_any_test.go b/tests/integration/query/json/with_any_test.go new file mode 100644 index 0000000000..d38d3e83e8 --- /dev/null +++ b/tests/integration/query/json/with_any_test.go @@ -0,0 +1,59 @@ +// 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 json + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryJSON_WithAnyFilter_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple JSON array, filtered any of string array", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: `type Users { + name: String + custom: JSON + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "custom": [1, false, "second", {"one": 1}, [1, 2]] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "custom": [null, false, "second", {"one": 1}, [1, 2]] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {custom: {_any: {_eq: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Fred", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/simple/with_filter/with_eq_json_test.go b/tests/integration/query/json/with_eq_test.go similarity index 91% rename from tests/integration/query/simple/with_filter/with_eq_json_test.go rename to tests/integration/query/json/with_eq_test.go index 3f85a033cb..4434412a1a 100644 --- a/tests/integration/query/simple/with_filter/with_eq_json_test.go +++ b/tests/integration/query/json/with_eq_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package simple +package json import ( "testing" @@ -16,7 +16,7 @@ import ( testUtils "github.com/sourcenetwork/defradb/tests/integration" ) -func TestQuerySimple_WithEqOpOnJSONFieldWithObject_ShouldFilter(t *testing.T) { +func TestQueryJSON_WithEqualFilterWithObject_ShouldFilter(t *testing.T) { test := testUtils.TestCase{ Actions: []any{ testUtils.SchemaUpdate{ @@ -69,7 +69,7 @@ func TestQuerySimple_WithEqOpOnJSONFieldWithObject_ShouldFilter(t *testing.T) { testUtils.ExecuteTestCase(t, test) } -func TestQuerySimple_WithEqOpOnJSONFieldWithNestedObjects_ShouldFilter(t *testing.T) { +func TestQueryJSON_WithEqualFilterWithNestedObjects_ShouldFilter(t *testing.T) { test := testUtils.TestCase{ Actions: []any{ testUtils.SchemaUpdate{ @@ -122,7 +122,7 @@ func TestQuerySimple_WithEqOpOnJSONFieldWithNestedObjects_ShouldFilter(t *testin testUtils.ExecuteTestCase(t, test) } -func TestQuerySimple_WithEqOpOnJSONFieldWithNullValue_ShouldFilter(t *testing.T) { +func TestQueryJSON_WithEqualFilterWithNullValue_ShouldFilter(t *testing.T) { test := testUtils.TestCase{ Actions: []any{ testUtils.SchemaUpdate{ diff --git a/tests/integration/query/json/with_ge_test.go b/tests/integration/query/json/with_ge_test.go new file mode 100644 index 0000000000..bfb574170e --- /dev/null +++ b/tests/integration/query/json/with_ge_test.go @@ -0,0 +1,499 @@ +// 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 json + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryJSON_WithGreaterEqualFilterWithEqualValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _ge filter equal value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_ge: 32}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "David", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterEqualFilterWithGreaterValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _ge filter greater value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_ge: 31}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "David", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterEqualFilterWithNullValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _ge filter null value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_ge: null}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "David", + }, + { + "Name": "John", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterEqualFilterWithNestedEqualValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _ge filter nested equal value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": {"age": 32} + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_ge: 32}}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "David", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterEqualFilterWithNestedGreaterValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _ge nested filter nested greater value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": {"age": 32} + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_ge: 31}}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "David", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterEqualFilterWithNestedNullValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _ge filter nested null value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_ge: null}}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + }, + { + "Name": "David", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterEqualFilterWithBoolValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _ge filter bool value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_ge: true}}) { + Name + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: bool`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterEqualFilterWithStringValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _ge filter string value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_ge: ""}}) { + Name + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: string`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterEqualFilterWithObjectValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _ge filter object value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_ge: {one: 1}}}) { + Name + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: map[string]interface {}`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterEqualFilterWithArrayValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _ge filter array value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_ge: [1, 2]}}) { + Name + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: []interface {}`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterEqualFilterWithAllTypes_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _ge filter all types", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Shahzad", + "Custom": "32" + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Andy", + "Custom": [1, 2] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Fred", + "Custom": {"one": 1} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": false + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_ge: 32}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "David", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/json/with_gt_test.go b/tests/integration/query/json/with_gt_test.go new file mode 100644 index 0000000000..3a2972320b --- /dev/null +++ b/tests/integration/query/json/with_gt_test.go @@ -0,0 +1,497 @@ +// 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 json + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryJSON_WithGreaterThanFilterBlockWithGreaterValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), greater than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_gt: 20}}) { + Name + Custom + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + "Custom": int64(21), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterThanFilterBlockWithLesserValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), greater than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_gt: 22}}) { + Name + Custom + } + }`, + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterThanFilterBlockWithNullFilterValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic JSON greater than filter, with null filter value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_gt: null}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterThanFilterBlockWithNestedGreaterValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), nested greater than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": {"age": 19} + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_gt: 20}}}) { + Name + Custom + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + "Custom": map[string]any{ + "age": uint64(21), + }, + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterThanFilterBlockWithNestedLesserValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), nested greater than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": {"age": 19} + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_gt: 22}}}) { + Name + Custom + } + }`, + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterThanFilterBlockWithNestedNullFilterValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic JSON greater than filter, with nested null filter value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_gt: null}}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterThanFilterBlockWithBoolValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), greater than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_gt: false}}) { + Name + Custom + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: bool`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterThanFilterBlockWithStringValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), greater than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_gt: ""}}) { + Name + Custom + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: string`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterThanFilterBlockWithObjectValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), greater than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_gt: {one: 1}}}) { + Name + Custom + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: map[string]interface {}`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterThanFilterBlockWithArrayValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), greater than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_gt: [1,2]}}) { + Name + Custom + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: []interface {}`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithGreaterThanFilterWithAllTypes_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _gt filter all types", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Shahzad", + "Custom": "32" + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Andy", + "Custom": [1, 2] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Fred", + "Custom": {"one": 1} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": false + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_gt: 30}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "David", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/simple/with_filter/with_in_json_test.go b/tests/integration/query/json/with_in_test.go similarity index 93% rename from tests/integration/query/simple/with_filter/with_in_json_test.go rename to tests/integration/query/json/with_in_test.go index 568862ee52..335a81d4ae 100644 --- a/tests/integration/query/simple/with_filter/with_in_json_test.go +++ b/tests/integration/query/json/with_in_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package simple +package json import ( "testing" @@ -16,7 +16,7 @@ import ( testUtils "github.com/sourcenetwork/defradb/tests/integration" ) -func TestQuerySimple_WithInOpOnJSONField_ShouldFilter(t *testing.T) { +func TestQueryJSON_WithInFilter_ShouldFilter(t *testing.T) { test := testUtils.TestCase{ Actions: []any{ testUtils.SchemaUpdate{ diff --git a/tests/integration/query/json/with_le_test.go b/tests/integration/query/json/with_le_test.go new file mode 100644 index 0000000000..49ae99e51b --- /dev/null +++ b/tests/integration/query/json/with_le_test.go @@ -0,0 +1,493 @@ +// 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 json + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryJSON_WithLesserEqualFilterWithEqualValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _le filter equal value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_le: 21}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserEqualFilterWithLesserValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _le filter lesser value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_le: 31}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserEqualFilterWithNullValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _le filter null value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_le: null}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "David", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserEqualFilterWithNestedEqualValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _le filter nested equal value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": {"age": 32} + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_le: 21}}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserEqualFilterWithNestedLesserValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _le nested filter nested lesser value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": {"age": 32} + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_le: 31}}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserEqualFilterWithNestedNullValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _le filter nested null value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_le: null}}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "David", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserEqualFilterWithBoolValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _le filter bool value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_le: true}}) { + Name + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: bool`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserEqualFilterWithStringValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _le filter string value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_le: ""}}) { + Name + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: string`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserEqualFilterWithObjectValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _le filter object value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_le: {one: 1}}}) { + Name + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: map[string]interface {}`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserEqualFilterWithArrayValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _le filter array value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_le: [1, 2]}}) { + Name + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: []interface {}`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserEqualFilterWithAllTypes_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _le filter all types", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Shahzad", + "Custom": "32" + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Andy", + "Custom": [1, 2] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Fred", + "Custom": {"one": 1} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": false + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_le: 32}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "David", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/json/with_like_test.go b/tests/integration/query/json/with_like_test.go new file mode 100644 index 0000000000..bb7ed62507 --- /dev/null +++ b/tests/integration/query/json/with_like_test.go @@ -0,0 +1,77 @@ +// 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 json + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryJSON_WithLikeFilter_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + custom: JSON + } + `, + }, + testUtils.CreateDoc{ + DocMap: map[string]any{ + "custom": "Daenerys Stormborn of House Targaryen, the First of Her Name", + }, + }, + testUtils.CreateDoc{ + DocMap: map[string]any{ + "custom": "Viserys I Targaryen, King of the Andals", + }, + }, + testUtils.CreateDoc{ + Doc: `{ + "custom": [1, 2] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "custom": {"one": 1} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "custom": false + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {custom: {_like: "Daenerys%Name"}}) { + custom + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "custom": "Daenerys Stormborn of House Targaryen, the First of Her Name", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/json/with_lt_test.go b/tests/integration/query/json/with_lt_test.go new file mode 100644 index 0000000000..14a422d5ad --- /dev/null +++ b/tests/integration/query/json/with_lt_test.go @@ -0,0 +1,489 @@ +// 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 json + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryJSON_WithLesserThanFilterBlockWithGreaterValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), lesser than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_lt: 20}}) { + Name + Custom + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "Bob", + "Custom": int64(19), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserThanFilterBlockWithLesserValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), lesser than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_lt: 19}}) { + Name + Custom + } + }`, + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserThanFilterBlockWithNullFilterValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic JSON lesser than filter, with null filter value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_lt: null}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserThanFilterBlockWithNestedGreaterValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), nested lesser than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Custom": {"age": 19} + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_lt: 20}}}) { + Name + Custom + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "Bob", + "Custom": map[string]any{ + "age": uint64(19), + }, + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserThanFilterBlockWithNestedLesserValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), nested lesser than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Custom": {"age": 19} + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_lt: 19}}}) { + Name + Custom + } + }`, + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserThanFilterBlockWithNestedNullFilterValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic JSON lesser than filter, with nested null filter value", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": {"age": 21} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {age: {_lt: null}}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserThanFilterBlockWithBoolValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), lesser than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_lt: false}}) { + Name + Custom + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: bool`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserThanFilterBlockWithStringValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), lesser than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_lt: ""}}) { + Name + Custom + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: string`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserThanFilterBlockWithObjectValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), lesser than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_lt: {one: 1}}}) { + Name + Custom + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: map[string]interface {}`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserThanFilterBlockWithArrayValue_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with basic filter(custom), lesser than", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": 21 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Custom": 19 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_lt: [1,2]}}) { + Name + Custom + } + }`, + ExpectedError: `unexpected type. Property: condition, Actual: []interface {}`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithLesserThanFilterWithAllTypes_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with JSON _lt filter all types", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Shahzad", + "Custom": "32" + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Andy", + "Custom": [1, 2] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Fred", + "Custom": {"one": 1} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "John", + "Custom": false + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "David", + "Custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {Custom: {_lt: 33}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "David", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/json/with_ne_test.go b/tests/integration/query/json/with_ne_test.go new file mode 100644 index 0000000000..6a4d619552 --- /dev/null +++ b/tests/integration/query/json/with_ne_test.go @@ -0,0 +1,165 @@ +// 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 json + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryJSON_WithNotEqualFilterWithObject_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "John", + "custom": { + "tree": "maple", + "age": 250 + } + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Andy", + "custom": { + "tree": "oak", + "age": 450 + } + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "custom": null + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {custom: {_ne: {tree:"oak",age:450}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + {"name": "John"}, + {"name": "Shahzad"}, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithNotEqualFilterWithNestedObjects_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "John", + "custom": { + "level_1": { + "level_2": { + "level_3": [true, false] + } + } + } + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Andy", + "custom": { + "level_1": { + "level_2": { + "level_3": [false, true] + } + } + } + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {custom: {_ne: {level_1: {level_2: {level_3: [true, false]}}}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + {"name": "Andy"}, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQueryJSON_WithNotEqualFilterWithNullValue_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "John", + "custom": null + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Andy", + "custom": {} + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {custom: {_ne: null}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + {"name": "Andy"}, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/simple/with_filter/with_like_json_test.go b/tests/integration/query/json/with_nin_test.go similarity index 70% rename from tests/integration/query/simple/with_filter/with_like_json_test.go rename to tests/integration/query/json/with_nin_test.go index f77041ada0..12cfb4d650 100644 --- a/tests/integration/query/simple/with_filter/with_like_json_test.go +++ b/tests/integration/query/json/with_nin_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package simple +package json import ( "testing" @@ -16,7 +16,7 @@ import ( testUtils "github.com/sourcenetwork/defradb/tests/integration" ) -func TestQuerySimple_WithLikeOpOnJSONField_ShouldFilter(t *testing.T) { +func TestQueryJSON_WithNotInFilter_ShouldFilter(t *testing.T) { test := testUtils.TestCase{ Actions: []any{ testUtils.SchemaUpdate{ @@ -28,26 +28,32 @@ func TestQuerySimple_WithLikeOpOnJSONField_ShouldFilter(t *testing.T) { `, }, testUtils.CreateDoc{ - DocMap: map[string]any{ - "name": "John", - "custom": "{\"tree\": \"maple\", \"age\": 250}", - }, + Doc: `{ + "name": "John", + "custom": { + "tree": "maple", + "age": 250 + } + }`, }, testUtils.CreateDoc{ - DocMap: map[string]any{ - "name": "Andy", - "custom": "{\"tree\": \"oak\", \"age\": 450}", - }, + Doc: `{ + "name": "Andy", + "custom": { + "tree": "oak", + "age": 450 + } + }`, }, testUtils.Request{ Request: `query { - Users(filter: {custom: {_like: "%oak%"}}) { + Users(filter: {custom: {_nin: [{tree:"oak",age:450}]}}) { name } }`, Results: map[string]any{ "Users": []map[string]any{ - {"name": "Andy"}, + {"name": "John"}, }, }, }, diff --git a/tests/integration/query/json/with_nlike_test.go b/tests/integration/query/json/with_nlike_test.go new file mode 100644 index 0000000000..db0615b2ca --- /dev/null +++ b/tests/integration/query/json/with_nlike_test.go @@ -0,0 +1,89 @@ +// 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 json + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryJSON_WithNotLikeFilter_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + custom: JSON + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "custom": "Daenerys Stormborn of House Targaryen, the First of Her Name" + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "custom": "Viserys I Targaryen, King of the Andals" + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "custom": [1, 2] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "custom": {"one": 1} + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "custom": false + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "custom": 32 + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {custom: {_nlike: "%Stormborn%"}}) { + custom + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "custom": uint64(32), + }, + { + "custom": "Viserys I Targaryen, King of the Andals", + }, + { + "custom": map[string]any{"one": uint64(1)}, + }, + { + "custom": []any{uint64(1), uint64(2)}, + }, + { + "custom": false, + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/json/with_none_test.go b/tests/integration/query/json/with_none_test.go new file mode 100644 index 0000000000..2355810423 --- /dev/null +++ b/tests/integration/query/json/with_none_test.go @@ -0,0 +1,59 @@ +// 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 json + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryJSON_WithNoneFilter_ShouldFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple JSON array, filtered none of string array", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: `type Users { + name: String + custom: JSON + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Shahzad", + "custom": [1, false, "second", {"one": 1}, [1, 2]] + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Fred", + "custom": [null, false, "second", {"one": 1}, [1, 2]] + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {custom: {_none: {_eq: null}}}) { + name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/schema/filter_test.go b/tests/integration/schema/filter_test.go index c3dc47d668..a48ac2e296 100644 --- a/tests/integration/schema/filter_test.go +++ b/tests/integration/schema/filter_test.go @@ -287,3 +287,126 @@ var defaultBookArgsWithoutFilter = trimFields( }, testFilterForOneToOneSchemaArgProps, ) + +func TestSchemaFilterInputs_WithJSONField_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + custom: JSON + } + `, + }, + testUtils.IntrospectionRequest{ + Request: ` + query { + __schema { + queryType { + fields { + name + args { + name + type { + name + inputFields { + name + type { + name + ofType { + name + } + } + } + } + } + } + } + } + } + `, + ContainsData: map[string]any{ + "__schema": map[string]any{ + "queryType": map[string]any{ + "fields": []any{ + map[string]any{ + "name": "Users", + "args": append( + // default args without filter + trimFields( + fields{ + cidArg, + docIDArg, + showDeletedArg, + groupByArg, + limitArg, + offsetArg, + buildOrderArg("Users"), + }, + map[string]any{ + "name": struct{}{}, + "type": map[string]any{ + "name": struct{}{}, + "inputFields": struct{}{}, + }, + }, + ), + map[string]any{ + "name": "filter", + "type": map[string]any{ + "name": "UsersFilterArg", + "inputFields": []any{ + map[string]any{ + "name": "_and", + "type": map[string]any{ + "name": nil, + "ofType": map[string]any{ + "name": nil, + }, + }, + }, + map[string]any{ + "name": "_docID", + "type": map[string]any{ + "name": "IDOperatorBlock", + "ofType": nil, + }, + }, + map[string]any{ + "name": "_not", + "type": map[string]any{ + "name": "UsersFilterArg", + "ofType": nil, + }, + }, + map[string]any{ + "name": "_or", + "type": map[string]any{ + "name": nil, + "ofType": map[string]any{ + "name": nil, + }, + }, + }, + map[string]any{ + "name": "custom", + "type": map[string]any{ + "name": "JSON", + "ofType": nil, + }, + }, + }, + }, + }, + ).Tidy(), + }, + }, + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/schema/simple_test.go b/tests/integration/schema/simple_test.go index c3603b4b11..739e7da49a 100644 --- a/tests/integration/schema/simple_test.go +++ b/tests/integration/schema/simple_test.go @@ -329,3 +329,49 @@ func TestSchemaSimpleCreatesSchemaGivenTypeWithBlobField(t *testing.T) { testUtils.ExecuteTestCase(t, test) } + +func TestSchemaSimple_WithJSONField_CreatesSchemaGivenType(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + data: JSON + } + `, + }, + testUtils.IntrospectionRequest{ + Request: ` + query { + __type (name: "Users") { + name + fields { + name + type { + name + kind + } + } + } + } + `, + ExpectedData: map[string]any{ + "__type": map[string]any{ + "name": "Users", + "fields": DefaultFields.Append( + Field{ + "name": "data", + "type": map[string]any{ + "kind": "SCALAR", + "name": "JSON", + }, + }, + ).Tidy(), + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +}