Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support PatchSchema relational field kind substitution #1777

Merged
merged 4 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions db/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
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"
errFieldKindDoesNotMatchFieldSchema string = "field Kind does not match field Schema"
errSchemaNotFound string = "no schema found for given name"
errDocumentAlreadyExists string = "a document with the given dockey already exists"
errDocumentDeleted string = "a document with the given dockey has been deleted"
Expand Down Expand Up @@ -138,6 +139,7 @@ var (
ErrInvalidCRDTType = errors.New(errInvalidCRDTType)
ErrCannotDeleteField = errors.New(errCannotDeleteField)
ErrFieldKindNotFound = errors.New(errFieldKindNotFound)
ErrFieldKindDoesNotMatchFieldSchema = errors.New(errFieldKindDoesNotMatchFieldSchema)
ErrSchemaNotFound = errors.New(errSchemaNotFound)
ErrIndexMissingFields = errors.New(errIndexMissingFields)
ErrIndexFieldMissingName = errors.New(errIndexFieldMissingName)
Expand Down Expand Up @@ -387,6 +389,14 @@ func NewErrFieldKindNotFound(kind string) error {
)
}

func NewErrFieldKindDoesNotMatchFieldSchema(kind string, schema string) error {
return errors.New(
errFieldKindDoesNotMatchFieldSchema,
errors.NewKV("Kind", kind),
errors.NewKV("Schema", schema),
)
}

func NewErrSchemaNotFound(name string, schema string) error {
return errors.New(
errSchemaNotFound,
Expand Down
94 changes: 68 additions & 26 deletions db/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (

jsonpatch "github.com/evanphx/json-patch/v5"

"github.com/sourcenetwork/immutable"

"github.com/sourcenetwork/defradb/client"
"github.com/sourcenetwork/defradb/datastore"
)
Expand Down Expand Up @@ -97,13 +99,14 @@ 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)

collectionsByName, err := db.getCollectionsByName(ctx, txn)
if err != nil {
return err
}

collectionsByName, err := db.getCollectionsByName(ctx, txn)
// Here we swap out any string representations of enums for their integer values
patch, err = substituteSchemaPatch(patch, collectionsByName)
if err != nil {
return err
}
Expand Down Expand Up @@ -162,14 +165,18 @@ func (db *db) getCollectionsByName(
//
// For example Field [FieldKind] string representations will be replaced by the raw integer
// value.
func substituteSchemaPatch(patch jsonpatch.Patch) (jsonpatch.Patch, error) {
func substituteSchemaPatch(
patch jsonpatch.Patch,
collectionsByName map[string]client.CollectionDescription,
) (jsonpatch.Patch, error) {
for _, patchOperation := range patch {
path, err := patchOperation.Path()
if err != nil {
return nil, err
}

if value, hasValue := patchOperation["value"]; hasValue {
var newPatchValue immutable.Option[any]
if isField(path) {
// We unmarshal the full field-value into a map to ensure that all user
// specified properties are maintained.
Expand All @@ -180,19 +187,20 @@ func substituteSchemaPatch(patch jsonpatch.Patch) (jsonpatch.Patch, error) {
}

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
}
substitute, collectionName, err := getSubstituteFieldKind(kind, collectionsByName)
if err != nil {
return nil, err
}

substituteValue := json.RawMessage(substituteField)
patchOperation["value"] = &substituteValue
} else {
return nil, NewErrFieldKindNotFound(kind)
field["Kind"] = substitute
if collectionName != "" {
if field["Schema"] != nil && field["Schema"] != collectionName {
return nil, NewErrFieldKindDoesNotMatchFieldSchema(kind, field["Schema"].(string))
}
field["Schema"] = collectionName
}

newPatchValue = immutable.Some[any](field)
}
} else if isFieldKind(path) {
var kind any
Expand All @@ -202,26 +210,60 @@ func substituteSchemaPatch(patch jsonpatch.Patch) (jsonpatch.Patch, error) {
}

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)
substitute, _, err := getSubstituteFieldKind(kind, collectionsByName)
if err != nil {
return nil, err
}

newPatchValue = immutable.Some[any](substitute)
}
}

if newPatchValue.HasValue() {
substitute, err := json.Marshal(newPatchValue.Value())
if err != nil {
return nil, err
}

substitutedValue := json.RawMessage(substitute)
patchOperation["value"] = &substitutedValue
}
}
}

return patch, nil
}

// getSubstituteFieldKind checks and attempts to get the underlying integer value for the given string
// Field Kind value. It will return the value if one is found, else returns an [ErrFieldKindNotFound].
//
// If the value represents a foreign relation the collection name will also be returned.
func getSubstituteFieldKind(
kind string,
collectionsByName map[string]client.CollectionDescription,
) (client.FieldKind, string, error) {
substitute, substituteFound := client.FieldKindStringToEnumMapping[kind]
if substituteFound {
return substitute, "", nil
} else {
var collectionName string
var substitute client.FieldKind
if len(kind) > 0 && kind[0] == '[' && kind[len(kind)-1] == ']' {
collectionName = kind[1 : len(kind)-1]
substitute = client.FieldKind_FOREIGN_OBJECT_ARRAY
} else {
collectionName = kind
substitute = client.FieldKind_FOREIGN_OBJECT
}

if _, substituteFound := collectionsByName[collectionName]; substituteFound {
return substitute, collectionName, nil
}

return 0, "", NewErrFieldKindNotFound(kind)
}
}

// isField returns true if the given path points to a FieldDescription.
func isField(path string) bool {
path = strings.TrimPrefix(path, "/")
Expand Down
Loading