Skip to content

Commit

Permalink
feat: Add field kind substitution for PatchSchema (#1223)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewSisley authored Mar 27, 2023
1 parent 23bef97 commit 9f6a2c6
Show file tree
Hide file tree
Showing 20 changed files with 857 additions and 0 deletions.
3 changes: 3 additions & 0 deletions client/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ type Store interface {
// The collections (including the schema version ID) will only be updated if any changes have actually
// been made, if the net result of the patch matches the current persisted description then no changes
// will be applied.
//
// Field [FieldKind] values may be provided in either their raw integer form, or as string as per
// [FieldKindStringToEnumMapping].
PatchSchema(context.Context, string) error

CreateCollection(context.Context, CollectionDescription) (Collection, error)
Expand Down
24 changes: 24 additions & 0 deletions client/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,30 @@ const (
FieldKind_NILLABLE_STRING_ARRAY FieldKind = 21
)

// FieldKindStringToEnumMapping maps string representations of [FieldKind] values to
// their enum values.
//
// It is currently used to by [db.PatchSchema] to allow string representations of
// [FieldKind] to be provided instead of their raw int values. This useage may expand
// in the future. They currently roughly correspond to the GQL field types, but this
// equality is not guarenteed.
var FieldKindStringToEnumMapping = map[string]FieldKind{
"ID": FieldKind_DocKey,
"Boolean": FieldKind_BOOL,
"[Boolean]": FieldKind_NILLABLE_BOOL_ARRAY,
"[Boolean!]": FieldKind_BOOL_ARRAY,
"Integer": FieldKind_INT,
"[Integer]": FieldKind_NILLABLE_INT_ARRAY,
"[Integer!]": FieldKind_INT_ARRAY,
"DateTime": FieldKind_DATETIME,
"Float": FieldKind_FLOAT,
"[Float]": FieldKind_NILLABLE_FLOAT_ARRAY,
"[Float!]": FieldKind_FLOAT_ARRAY,
"String": FieldKind_STRING,
"[String]": FieldKind_NILLABLE_STRING_ARRAY,
"[String!]": FieldKind_STRING_ARRAY,
}

// RelationType describes the type of relation between two types.
type RelationType uint8

Expand Down
9 changes: 9 additions & 0 deletions db/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
errCannotMoveField string = "moving fields is not currently supported"
errInvalidCRDTType string = "only default or LWW (last writer wins) CRDT types are supported"
errCannotDeleteField string = "deleting an existing field is not supported"
errFieldKindNotFound string = "no type found for given name"
)

var (
Expand Down Expand Up @@ -79,6 +80,7 @@ var (
ErrCannotMoveField = errors.New(errCannotMoveField)
ErrInvalidCRDTType = errors.New(errInvalidCRDTType)
ErrCannotDeleteField = errors.New(errCannotDeleteField)
ErrFieldKindNotFound = errors.New(errFieldKindNotFound)
)

// NewErrFailedToGetHeads returns a new error indicating that the heads of a document
Expand Down Expand Up @@ -168,6 +170,13 @@ func NewErrCannotAddRelationalField(name string, kind client.FieldKind) error {
)
}

func NewErrFieldKindNotFound(kind string) error {
return errors.New(
errFieldKindNotFound,
errors.NewKV("Kind", kind),
)
}

func NewErrDuplicateField(name string) error {
return errors.New(errDuplicateField, errors.NewKV("Name", name))
}
Expand Down
88 changes: 88 additions & 0 deletions db/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ func (db *db) patchSchema(ctx context.Context, txn datastore.Txn, patchString st
if err != nil {
return err
}
// Here we swap out any string representations of enums for their integer values
patch, err = substituteSchemaPatch(patch)
if err != nil {
return err
}

collectionsByName, err := db.getCollectionsByName(ctx, txn)
if err != nil {
Expand Down Expand Up @@ -144,3 +149,86 @@ func (db *db) getCollectionsByName(

return collectionsByName, nil
}

// substituteSchemaPatch handles any substitution of values that may be required before
// the patch can be applied.
//
// For example Field [FieldKind] string representations will be replaced by the raw integer
// value.
func substituteSchemaPatch(patch jsonpatch.Patch) (jsonpatch.Patch, error) {
for _, patchOperation := range patch {
path, err := patchOperation.Path()
if err != nil {
return nil, err
}

if value, hasValue := patchOperation["value"]; hasValue {
if isField(path) {
// We unmarshal the full field-value into a map to ensure that all user
// specified properties are maintained.
var field map[string]any
err = json.Unmarshal(*value, &field)
if err != nil {
return nil, err
}

if kind, isString := field["Kind"].(string); isString {
substitute, substituteFound := client.FieldKindStringToEnumMapping[kind]
if substituteFound {
field["Kind"] = substitute
substituteField, err := json.Marshal(field)
if err != nil {
return nil, err
}

substituteValue := json.RawMessage(substituteField)
patchOperation["value"] = &substituteValue
} else {
return nil, NewErrFieldKindNotFound(kind)
}
}
} else if isFieldKind(path) {
var kind any
err = json.Unmarshal(*value, &kind)
if err != nil {
return nil, err
}

if kind, isString := kind.(string); isString {
substitute, substituteFound := client.FieldKindStringToEnumMapping[kind]
if substituteFound {
substituteKind, err := json.Marshal(substitute)
if err != nil {
return nil, err
}

substituteValue := json.RawMessage(substituteKind)
patchOperation["value"] = &substituteValue
} else {
return nil, NewErrFieldKindNotFound(kind)
}
}
}
}
}

return patch, nil
}

// isField returns true if the given path points to a FieldDescription.
func isField(path string) bool {
path = strings.TrimPrefix(path, "/")
elements := strings.Split(path, "/")
//nolint:goconst
return len(elements) == 4 && elements[len(elements)-2] == "Fields" && elements[len(elements)-3] == "Schema"
}

// isField returns true if the given path points to a FieldDescription.Kind property.
func isFieldKind(path string) bool {
path = strings.TrimPrefix(path, "/")
elements := strings.Split(path, "/")
return len(elements) == 5 &&
elements[len(elements)-1] == "Kind" &&
elements[len(elements)-3] == "Fields" &&
elements[len(elements)-4] == "Schema"
}
44 changes: 44 additions & 0 deletions tests/integration/schema/updates/add/field/kind/bool_array_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,47 @@ func TestSchemaUpdatesAddFieldKindBoolArrayWithCreate(t *testing.T) {
}
testUtils.ExecuteTestCase(t, []string{"Users"}, test)
}

func TestSchemaUpdatesAddFieldKindBoolArraySubstitutionWithCreate(t *testing.T) {
test := testUtils.TestCase{
Description: "Test schema update, add field with kind bool array substitution with create",
Actions: []any{
testUtils.SchemaUpdate{
Schema: `
type Users {
Name: String
}
`,
},
testUtils.SchemaPatch{
Patch: `
[
{ "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": "[Boolean!]"} }
]
`,
},
testUtils.CreateDoc{
CollectionID: 0,
Doc: `{
"Name": "John",
"Foo": [true, false, true]
}`,
},
testUtils.Request{
Request: `query {
Users {
Name
Foo
}
}`,
Results: []map[string]any{
{
"Name": "John",
"Foo": []bool{true, false, true},
},
},
},
},
}
testUtils.ExecuteTestCase(t, []string{"Users"}, test)
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,47 @@ func TestSchemaUpdatesAddFieldKindNillableBoolArrayWithCreate(t *testing.T) {
}
testUtils.ExecuteTestCase(t, []string{"Users"}, test)
}

func TestSchemaUpdatesAddFieldKindNillableBoolArraySubstitutionWithCreate(t *testing.T) {
test := testUtils.TestCase{
Description: "Test schema update, add field with kind nillable bool array substitution with create",
Actions: []any{
testUtils.SchemaUpdate{
Schema: `
type Users {
Name: String
}
`,
},
testUtils.SchemaPatch{
Patch: `
[
{ "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": "[Boolean]"} }
]
`,
},
testUtils.CreateDoc{
CollectionID: 0,
Doc: `{
"Name": "John",
"Foo": [true, false, null]
}`,
},
testUtils.Request{
Request: `query {
Users {
Name
Foo
}
}`,
Results: []map[string]any{
{
"Name": "John",
"Foo": []immutable.Option[bool]{immutable.Some(true), immutable.Some(false), immutable.None[bool]()},
},
},
},
},
}
testUtils.ExecuteTestCase(t, []string{"Users"}, test)
}
44 changes: 44 additions & 0 deletions tests/integration/schema/updates/add/field/kind/bool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,47 @@ func TestSchemaUpdatesAddFieldKindBoolWithCreate(t *testing.T) {
}
testUtils.ExecuteTestCase(t, []string{"Users"}, test)
}

func TestSchemaUpdatesAddFieldKindBoolSubstitutionWithCreate(t *testing.T) {
test := testUtils.TestCase{
Description: "Test schema update, add field with kind bool substitution with create",
Actions: []any{
testUtils.SchemaUpdate{
Schema: `
type Users {
Name: String
}
`,
},
testUtils.SchemaPatch{
Patch: `
[
{ "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": "Boolean"} }
]
`,
},
testUtils.CreateDoc{
CollectionID: 0,
Doc: `{
"Name": "John",
"Foo": true
}`,
},
testUtils.Request{
Request: `query {
Users {
Name
Foo
}
}`,
Results: []map[string]any{
{
"Name": "John",
"Foo": true,
},
},
},
},
}
testUtils.ExecuteTestCase(t, []string{"Users"}, test)
}
44 changes: 44 additions & 0 deletions tests/integration/schema/updates/add/field/kind/datetime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,47 @@ func TestSchemaUpdatesAddFieldKindDateTimeWithCreate(t *testing.T) {
}
testUtils.ExecuteTestCase(t, []string{"Users"}, test)
}

func TestSchemaUpdatesAddFieldKindDateTimeSubstitutionWithCreate(t *testing.T) {
test := testUtils.TestCase{
Description: "Test schema update, add field with kind datetime substitution with create",
Actions: []any{
testUtils.SchemaUpdate{
Schema: `
type Users {
Name: String
}
`,
},
testUtils.SchemaPatch{
Patch: `
[
{ "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": "DateTime"} }
]
`,
},
testUtils.CreateDoc{
CollectionID: 0,
Doc: `{
"Name": "John",
"Foo": "2017-07-23T03:46:56.647Z"
}`,
},
testUtils.Request{
Request: `query {
Users {
Name
Foo
}
}`,
Results: []map[string]any{
{
"Name": "John",
"Foo": "2017-07-23T03:46:56.647Z",
},
},
},
},
}
testUtils.ExecuteTestCase(t, []string{"Users"}, test)
}
Loading

0 comments on commit 9f6a2c6

Please sign in to comment.