diff --git a/db/base/descriptions.go b/db/base/descriptions.go index 9938c93d8e..0448d38e0c 100644 --- a/db/base/descriptions.go +++ b/db/base/descriptions.go @@ -105,12 +105,16 @@ const ( FieldKind_None FieldKind = iota FieldKind_DocKey FieldKind_BOOL + FieldKind_BOOL_ARRAY FieldKind_INT + FieldKind_INT_ARRAY FieldKind_FLOAT + FieldKind_FLOAT_ARRAY FieldKind_DECIMNAL FieldKind_DATE FieldKind_TIMESTAMP FieldKind_STRING + FieldKind_STRING_ARRAY FieldKind_BYTES FieldKind_OBJECT // Embedded object within the type FieldKind_OBJECT_ARRAY // Array of embedded objects diff --git a/db/collection_update.go b/db/collection_update.go index 3a199e7cae..d3db613dad 100644 --- a/db/collection_update.go +++ b/db/collection_update.go @@ -436,10 +436,63 @@ func validateFieldSchema(val interface{}, field base.FieldDescription) (interfac switch field.Kind { case base.FieldKind_DocKey, base.FieldKind_STRING: cval, ok = val.(string) + case base.FieldKind_STRING_ARRAY: + if val == nil { + ok = true + cval = nil + break + } + untypedCollection := val.([]interface{}) + stringArray := make([]string, len(untypedCollection)) + for i, value := range untypedCollection { + if value == nil { + stringArray[i] = "" + continue + } + stringArray[i], ok = value.(string) + if !ok { + return nil, fmt.Errorf("Failed to cast value: %v of type: %T to string", value, value) + } + } + ok = true + cval = stringArray case base.FieldKind_BOOL: cval, ok = val.(bool) + case base.FieldKind_BOOL_ARRAY: + if val == nil { + ok = true + cval = nil + break + } + untypedCollection := val.([]interface{}) + boolArray := make([]bool, len(untypedCollection)) + for i, value := range untypedCollection { + boolArray[i], ok = value.(bool) + if !ok { + return nil, fmt.Errorf("Failed to cast value: %v of type: %T to bool", value, value) + } + } + ok = true + cval = boolArray case base.FieldKind_FLOAT, base.FieldKind_DECIMNAL: cval, ok = val.(float64) + case base.FieldKind_FLOAT_ARRAY: + if val == nil { + ok = true + cval = nil + break + } + untypedCollection := val.([]interface{}) + floatArray := make([]float64, len(untypedCollection)) + for i, value := range untypedCollection { + floatArray[i], ok = value.(float64) + if !ok { + return nil, fmt.Errorf("Failed to cast value: %v of type: %T to float64", value, value) + } + } + ok = true + cval = floatArray + case base.FieldKind_DATE: var sval string sval, ok = val.(string) @@ -451,6 +504,23 @@ func validateFieldSchema(val interface{}, field base.FieldDescription) (interfac return nil, ErrInvalidMergeValueType } cval = int64(fval) + case base.FieldKind_INT_ARRAY: + if val == nil { + ok = true + cval = nil + break + } + untypedCollection := val.([]interface{}) + intArray := make([]int64, len(untypedCollection)) + for i, value := range untypedCollection { + valueAsFloat, castOk := value.(float64) + if !castOk { + return nil, fmt.Errorf("Failed to cast value: %v of type: %T to float64", value, value) + } + intArray[i] = int64(valueAsFloat) + } + ok = true + cval = intArray case base.FieldKind_OBJECT, base.FieldKind_OBJECT_ARRAY, base.FieldKind_FOREIGN_OBJECT, base.FieldKind_FOREIGN_OBJECT_ARRAY: err = errors.New("Merge doesn't support sub types yet") diff --git a/db/fetcher/fetcher.go b/db/fetcher/fetcher.go index 1d8bffa3b9..03b7933007 100644 --- a/db/fetcher/fetcher.go +++ b/db/fetcher/fetcher.go @@ -280,7 +280,8 @@ func (df *DocumentFetcher) processKV(kv *core.KeyValue) error { // secondary index is provided, we need to extract the indexed/implicit fields // from the KV pair. df.doc.Properties[fieldDesc] = &document.EncProperty{ - Raw: kv.Value, + Desc: fieldDesc, + Raw: kv.Value, } // @todo: Extract Index implicit/stored keys return nil diff --git a/db/tests/mutation/inline_array/update/simple_test.go b/db/tests/mutation/inline_array/update/simple_test.go new file mode 100644 index 0000000000..7867a52b19 --- /dev/null +++ b/db/tests/mutation/inline_array/update/simple_test.go @@ -0,0 +1,515 @@ +// Copyright 2020 Source Inc. +// +// 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 update + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/db/tests" + inlineArray "github.com/sourcenetwork/defradb/db/tests/mutation/inline_array" +) + +func TestMutationInlineArrayUpdateWithBooleans(t *testing.T) { + tests := []testUtils.QueryTestCase{ + { + Description: "Simple update mutation with boolean array, replace with nil", + Query: `mutation { + update_users(data: "{\"LikedIndexes\": null}") { + Name + LikedIndexes + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "LikedIndexes": [true, true, false, true] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "LikedIndexes": nil, + }, + }, + }, + { + Description: "Simple update mutation with boolean array, replace with empty", + Query: `mutation { + update_users(data: "{\"LikedIndexes\": []}") { + Name + LikedIndexes + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "LikedIndexes": [true, true, false, true] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "LikedIndexes": []bool{}, + }, + }, + }, + { + Description: "Simple update mutation with boolean array, replace with same size", + Query: `mutation { + update_users(data: "{\"LikedIndexes\": [true, false, true, false]}") { + Name + LikedIndexes + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "LikedIndexes": [true, true, false, true] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "LikedIndexes": []bool{true, false, true, false}, + }, + }, + }, + { + Description: "Simple update mutation with boolean array, replace with smaller size", + Query: `mutation { + update_users(data: "{\"LikedIndexes\": [false, true]}") { + Name + LikedIndexes + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "LikedIndexes": [true, true, false, true] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "LikedIndexes": []bool{false, true}, + }, + }, + }, + { + Description: "Simple update mutation with boolean array, replace with larger size", + Query: `mutation { + update_users(data: "{\"LikedIndexes\": [true, false, true, false, true, true]}") { + Name + LikedIndexes + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "LikedIndexes": [true, true, false, true] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "LikedIndexes": []bool{true, false, true, false, true, true}, + }, + }, + }, + } + + for _, test := range tests { + inlineArray.ExecuteTestCase(t, test) + } +} + +func TestMutationInlineArrayUpdateWithIntegers(t *testing.T) { + tests := []testUtils.QueryTestCase{ + { + Description: "Simple update mutation with integer array, replace with nil", + Query: `mutation { + update_users(data: "{\"FavouriteIntegers\": null}") { + Name + FavouriteIntegers + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteIntegers": [1, 2, 3, 5, 8] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteIntegers": nil, + }, + }, + }, + { + Description: "Simple update mutation with integer array, replace with empty", + Query: `mutation { + update_users(data: "{\"FavouriteIntegers\": []}") { + Name + FavouriteIntegers + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteIntegers": [1, 2, 3, 5, 8] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteIntegers": []int64{}, + }, + }, + }, + { + Description: "Simple update mutation with integer array, replace with same size, positive values", + Query: `mutation { + update_users(data: "{\"FavouriteIntegers\": [8, 5, 3, 2, 1]}") { + Name + FavouriteIntegers + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteIntegers": [1, 2, 3, 5, 8] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteIntegers": []int64{8, 5, 3, 2, 1}, + }, + }, + }, + { + Description: "Simple update mutation with integer array, replace with same size, positive to mixed values", + Query: `mutation { + update_users(data: "{\"FavouriteIntegers\": [-1, 2, -3, 5, -8]}") { + Name + FavouriteIntegers + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteIntegers": [1, 2, 3, 5, 8] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteIntegers": []int64{-1, 2, -3, 5, -8}, + }, + }, + }, + { + Description: "Simple update mutation with integer array, replace with smaller size, positive values", + Query: `mutation { + update_users(data: "{\"FavouriteIntegers\": [1, 2, 3]}") { + Name + FavouriteIntegers + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteIntegers": [1, 2, 3, 5, 8] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteIntegers": []int64{1, 2, 3}, + }, + }, + }, + { + Description: "Simple update mutation with integer array, replace with larger size, positive values", + Query: `mutation { + update_users(data: "{\"FavouriteIntegers\": [1, 2, 3, 5, 8, 13, 21]}") { + Name + FavouriteIntegers + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteIntegers": [1, 2, 3, 5, 8] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteIntegers": []int64{1, 2, 3, 5, 8, 13, 21}, + }, + }, + }, + } + + for _, test := range tests { + inlineArray.ExecuteTestCase(t, test) + } +} + +func TestMutationInlineArrayUpdateWithFloats(t *testing.T) { + tests := []testUtils.QueryTestCase{ + { + Description: "Simple update mutation with float array, replace with nil", + Query: `mutation { + update_users(data: "{\"FavouriteFloats\": null}") { + Name + FavouriteFloats + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteFloats": [3.1425, 0.00000000001, 10] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteFloats": nil, + }, + }, + }, + { + Description: "Simple update mutation with float array, replace with empty", + Query: `mutation { + update_users(data: "{\"FavouriteFloats\": []}") { + Name + FavouriteFloats + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteFloats": [3.1425, 0.00000000001, 10] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteFloats": []float64{}, + }, + }, + }, + { + Description: "Simple update mutation with float array, replace with same size", + Query: `mutation { + update_users(data: "{\"FavouriteFloats\": [3.1425, -0.00000000001, 1000000]}") { + Name + FavouriteFloats + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteFloats": [3.1425, 0.00000000001, 10] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteFloats": []float64{3.1425, -0.00000000001, 1000000}, + }, + }, + }, + { + Description: "Simple update mutation with float array, replace with smaller size", + Query: `mutation { + update_users(data: "{\"FavouriteFloats\": [3.14]}") { + Name + FavouriteFloats + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteFloats": [3.1425, 0.00000000001, 10] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteFloats": []float64{3.14}, + }, + }, + }, + { + Description: "Simple update mutation with float array, replace with larger size", + Query: `mutation { + update_users(data: "{\"FavouriteFloats\": [3.1425, 0.00000000001, -10, 6.626070]}") { + Name + FavouriteFloats + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteFloats": [3.1425, 0.00000000001, 10] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteFloats": []float64{3.1425, 0.00000000001, -10, 6.626070}, + }, + }, + }, + } + + for _, test := range tests { + inlineArray.ExecuteTestCase(t, test) + } +} + +func TestMutationInlineArrayUpdateWithStrings(t *testing.T) { + tests := []testUtils.QueryTestCase{ + { + Description: "Simple update mutation with string array, replace with nil", + Query: `mutation { + update_users(data: "{\"PreferredStrings\": null}") { + Name + PreferredStrings + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "PreferredStrings": ["", "the previous", "the first", "empty string"] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "PreferredStrings": nil, + }, + }, + }, + { + Description: "Simple update mutation with string array, replace with empty", + Query: `mutation { + update_users(data: "{\"PreferredStrings\": []}") { + Name + PreferredStrings + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "PreferredStrings": ["", "the previous", "the first", "empty string"] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "PreferredStrings": []string{}, + }, + }, + }, + { + Description: "Simple update mutation with string array, replace with same size", + Query: `mutation { + update_users(data: "{\"PreferredStrings\": [null, \"the previous\", \"the first\", \"null string\"]}") { + Name + PreferredStrings + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "PreferredStrings": ["", "the previous", "the first", "empty string"] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "PreferredStrings": []string{"", "the previous", "the first", "null string"}, + }, + }, + }, + { + Description: "Simple update mutation with string array, replace with smaller size", + Query: `mutation { + update_users(data: "{\"PreferredStrings\": [\"\", \"the first\"]}") { + Name + PreferredStrings + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "PreferredStrings": ["", "the previous", "the first", "empty string"] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "PreferredStrings": []string{"", "the first"}, + }, + }, + }, + { + Description: "Simple update mutation with string array, replace with larger size", + Query: `mutation { + update_users(data: "{\"PreferredStrings\": [\"\", \"the previous\", \"the first\", \"empty string\", \"blank string\", \"hitchi\"]}") { + Name + PreferredStrings + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "PreferredStrings": ["", "the previous", "the first", "empty string"] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "PreferredStrings": []string{"", "the previous", "the first", "empty string", "blank string", "hitchi"}, + }, + }, + }, + } + + for _, test := range tests { + inlineArray.ExecuteTestCase(t, test) + } +} diff --git a/db/tests/mutation/inline_array/utils.go b/db/tests/mutation/inline_array/utils.go new file mode 100644 index 0000000000..f53201cf50 --- /dev/null +++ b/db/tests/mutation/inline_array/utils.go @@ -0,0 +1,30 @@ +// Copyright 2020 Source Inc. +// +// 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/db/tests" +) + +var userCollectionGQLSchema = (` + type users { + Name: String + LikedIndexes: [Boolean] + FavouriteIntegers: [Int] + FavouriteFloats: [Float] + PreferredStrings: [String] + } +`) + +func ExecuteTestCase(t *testing.T, test testUtils.QueryTestCase) { + testUtils.ExecuteQueryTestCase(t, userCollectionGQLSchema, []string{"users"}, test) +} diff --git a/db/tests/query/inline_array/simple_test.go b/db/tests/query/inline_array/simple_test.go new file mode 100644 index 0000000000..5a987fed52 --- /dev/null +++ b/db/tests/query/inline_array/simple_test.go @@ -0,0 +1,360 @@ +// Copyright 2020 Source Inc. +// +// 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/db/tests" +) + +func TestQueryInlineArrayWithBooleans(t *testing.T) { + tests := []testUtils.QueryTestCase{ + { + Description: "Simple inline array with no filter, nil boolean array", + Query: `query { + users { + Name + LikedIndexes + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "LikedIndexes": null + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "LikedIndexes": nil, + }, + }, + }, + { + Description: "Simple inline array with no filter, empty boolean array", + Query: `query { + users { + Name + LikedIndexes + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "LikedIndexes": [] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "LikedIndexes": []bool{}, + }, + }, + }, + { + Description: "Simple inline array with no filter, booleans", + Query: `query { + users { + Name + LikedIndexes + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "LikedIndexes": [true, true, false, true] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "LikedIndexes": []bool{true, true, false, true}, + }, + }, + }, + } + + for _, test := range tests { + executeTestCase(t, test) + } +} + +func TestQueryInlineArrayWithIntegers(t *testing.T) { + tests := []testUtils.QueryTestCase{ + { + Description: "Simple inline array with no filter, nil integer array", + Query: `query { + users { + Name + FavouriteIntegers + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteIntegers": null + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteIntegers": nil, + }, + }, + }, + { + Description: "Simple inline array with no filter, empty integer array", + Query: `query { + users { + Name + FavouriteIntegers + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteIntegers": [] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteIntegers": []int64{}, + }, + }, + }, + { + Description: "Simple inline array with no filter, positive integers", + Query: `query { + users { + Name + FavouriteIntegers + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteIntegers": [1, 2, 3, 5, 8] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteIntegers": []int64{1, 2, 3, 5, 8}, + }, + }, + }, + { + Description: "Simple inline array with no filter, negative integers", + Query: `query { + users { + Name + FavouriteIntegers + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "Andy", + "FavouriteIntegers": [-1, -2, -3, -5, -8] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "Andy", + "FavouriteIntegers": []int64{-1, -2, -3, -5, -8}, + }, + }, + }, + { + Description: "Simple inline array with no filter, mixed integers", + Query: `query { + users { + Name + FavouriteIntegers + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "Shahzad", + "FavouriteIntegers": [-1, 2, -1, 1, 0] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "Shahzad", + "FavouriteIntegers": []int64{-1, 2, -1, 1, 0}, + }, + }, + }, + } + + for _, test := range tests { + executeTestCase(t, test) + } +} + +func TestQueryInlineArrayWithFloats(t *testing.T) { + tests := []testUtils.QueryTestCase{ + { + Description: "Simple inline array with no filter, nil float array", + Query: `query { + users { + Name + FavouriteFloats + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteFloats": null + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteFloats": nil, + }, + }, + }, + { + Description: "Simple inline array with no filter, empty float array", + Query: `query { + users { + Name + FavouriteFloats + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteFloats": [] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteFloats": []float64{}, + }, + }, + }, + { + Description: "Simple inline array with no filter, positive floats", + Query: `query { + users { + Name + FavouriteFloats + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteFloats": [3.1425, 0.00000000001, 10] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "FavouriteFloats": []float64{3.1425, 0.00000000001, 10}, + }, + }, + }, + } + + for _, test := range tests { + executeTestCase(t, test) + } +} + +func TestQueryInlineArrayWithStrings(t *testing.T) { + tests := []testUtils.QueryTestCase{ + { + Description: "Simple inline array with no filter, nil string array", + Query: `query { + users { + Name + PreferredStrings + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "PreferredStrings": null + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "PreferredStrings": nil, + }, + }, + }, + { + Description: "Simple inline array with no filter, empty string array", + Query: `query { + users { + Name + PreferredStrings + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "PreferredStrings": [] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "PreferredStrings": []string{}, + }, + }, + }, + { + Description: "Simple inline array with no filter, strings", + Query: `query { + users { + Name + PreferredStrings + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "PreferredStrings": ["", "the previous", "the first", "empty string"] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "PreferredStrings": []string{"", "the previous", "the first", "empty string"}, + }, + }, + }, + } + + for _, test := range tests { + executeTestCase(t, test) + } +} diff --git a/db/tests/query/inline_array/utils.go b/db/tests/query/inline_array/utils.go new file mode 100644 index 0000000000..16e6ad750c --- /dev/null +++ b/db/tests/query/inline_array/utils.go @@ -0,0 +1,30 @@ +// Copyright 2020 Source Inc. +// +// 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/db/tests" +) + +var userCollectionGQLSchema = (` + type users { + Name: String + LikedIndexes: [Boolean] + FavouriteIntegers: [Int] + FavouriteFloats: [Float] + PreferredStrings: [String] + } +`) + +func executeTestCase(t *testing.T, test testUtils.QueryTestCase) { + testUtils.ExecuteQueryTestCase(t, userCollectionGQLSchema, []string{"users"}, test) +} diff --git a/db/tests/query/inline_array/with_count_test.go b/db/tests/query/inline_array/with_count_test.go new file mode 100644 index 0000000000..452efcc1e4 --- /dev/null +++ b/db/tests/query/inline_array/with_count_test.go @@ -0,0 +1,97 @@ +// Copyright 2020 Source Inc. +// +// 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/db/tests" +) + +func TestQueryInlineIntegerArrayWithsWithCountAndNullArray(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Simple inline array with no filter, count of nil integer array", + Query: `query { + users { + Name + _count(field: FavouriteIntegers) + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteIntegers": null + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "_count": 0, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineIntegerArrayWithsWithCountAndEmptyArray(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Simple inline array with no filter, count of empty integer array", + Query: `query { + users { + Name + _count(field: FavouriteIntegers) + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "John", + "FavouriteIntegers": [] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "John", + "_count": 0, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryInlineIntegerArrayWithsWithCountAndPopulatedArray(t *testing.T) { + test := testUtils.QueryTestCase{ + Description: "Simple inline array with no filter, count of empty integer array", + Query: `query { + users { + Name + _count(field: FavouriteIntegers) + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "Name": "Shahzad", + "FavouriteIntegers": [-1, 2, -1, 1, 0] + }`)}, + }, + Results: []map[string]interface{}{ + { + "Name": "Shahzad", + "_count": 5, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/document/document.go b/document/document.go index 2608725334..710a1026ae 100644 --- a/document/document.go +++ b/document/document.go @@ -315,13 +315,9 @@ func (doc *Document) setAndParseType(field string, value interface{}) error { } // string, bool, and more - case string, bool: + case string, bool, []interface{}: doc.setCBOR(core.LWW_REGISTER, field, val) - // array - case []interface{}: - break - // sub object, recurse down. // @TODO: Object Definitions // You can use an object as a way to override defults diff --git a/document/encoded.go b/document/encoded.go index 5cc6c85b8d..580e46c082 100644 --- a/document/encoded.go +++ b/document/encoded.go @@ -10,6 +10,8 @@ package document import ( + "fmt" + "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/db/base" "github.com/sourcenetwork/defradb/document/key" @@ -37,6 +39,55 @@ func (e EncProperty) Decode() (core.CType, interface{}, error) { if err != nil { return ctype, nil, err } + + if array, isArray := val.([]interface{}); isArray { + var ok bool + switch e.Desc.Kind { + case base.FieldKind_BOOL_ARRAY: + boolArray := make([]bool, len(array)) + for i, untypedValue := range array { + boolArray[i], ok = untypedValue.(bool) + if !ok { + return ctype, nil, fmt.Errorf("Could not convert type: %T, value: %v to bool.", untypedValue, untypedValue) + } + } + val = boolArray + case base.FieldKind_INT_ARRAY: + intArray := make([]int64, len(array)) + for i, untypedValue := range array { + switch value := untypedValue.(type) { + case uint64: + intArray[i] = int64(value) + case int64: + intArray[i] = value + case float64: + intArray[i] = int64(value) + default: + return ctype, nil, fmt.Errorf("Could not convert type: %T, value: %v to int64.", untypedValue, untypedValue) + } + } + val = intArray + case base.FieldKind_FLOAT_ARRAY: + floatArray := make([]float64, len(array)) + for i, untypedValue := range array { + floatArray[i], ok = untypedValue.(float64) + if !ok { + return ctype, nil, fmt.Errorf("Could not convert type: %T, value: %v to float64.", untypedValue, untypedValue) + } + } + val = floatArray + case base.FieldKind_STRING_ARRAY: + stringArray := make([]string, len(array)) + for i, untypedValue := range array { + stringArray[i], ok = untypedValue.(string) + if !ok { + return ctype, nil, fmt.Errorf("Could not convert type: %T, value: %v to string.", untypedValue, untypedValue) + } + } + val = stringArray + } + } + return ctype, val, nil } diff --git a/query/graphql/schema/descriptions.go b/query/graphql/schema/descriptions.go index 166f4486a2..d6418ead86 100644 --- a/query/graphql/schema/descriptions.go +++ b/query/graphql/schema/descriptions.go @@ -46,10 +46,14 @@ var ( defaultCRDTForFieldKind = map[base.FieldKind]core.CType{ base.FieldKind_DocKey: core.LWW_REGISTER, base.FieldKind_BOOL: core.LWW_REGISTER, + base.FieldKind_BOOL_ARRAY: core.LWW_REGISTER, base.FieldKind_INT: core.LWW_REGISTER, + base.FieldKind_INT_ARRAY: core.LWW_REGISTER, base.FieldKind_FLOAT: core.LWW_REGISTER, + base.FieldKind_FLOAT_ARRAY: core.LWW_REGISTER, base.FieldKind_DATE: core.LWW_REGISTER, base.FieldKind_STRING: core.LWW_REGISTER, + base.FieldKind_STRING_ARRAY: core.LWW_REGISTER, base.FieldKind_FOREIGN_OBJECT: core.NONE_CRDT, base.FieldKind_FOREIGN_OBJECT_ARRAY: core.NONE_CRDT, } @@ -75,6 +79,18 @@ func gqlTypeToFieldKind(t gql.Type) base.FieldKind { case *gql.Object: return base.FieldKind_FOREIGN_OBJECT case *gql.List: + if scalar, isScalar := v.OfType.(*gql.Scalar); isScalar { + switch scalar.Name() { + case "Boolean": + return base.FieldKind_BOOL_ARRAY + case "Int": + return base.FieldKind_INT_ARRAY + case "Float": + return base.FieldKind_FLOAT_ARRAY + case "String": + return base.FieldKind_STRING_ARRAY + } + } return base.FieldKind_FOREIGN_OBJECT_ARRAY }