From bd5cd38f543dd682c0dbbca1069756abb28c39ae Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Wed, 2 Aug 2023 14:07:24 -0400 Subject: [PATCH 1/4] Pass existing collections around instead of re-fetching Is quicker --- db/collection.go | 19 ++++++++++--------- db/schema.go | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/db/collection.go b/db/collection.go index 3430684697..17fbc0e340 100644 --- a/db/collection.go +++ b/db/collection.go @@ -230,9 +230,10 @@ func (db *db) createCollection( func (db *db) updateCollection( ctx context.Context, txn datastore.Txn, + existingDescriptionsByName map[string]client.CollectionDescription, desc client.CollectionDescription, ) (client.Collection, error) { - hasChanged, err := db.validateUpdateCollection(ctx, txn, desc) + hasChanged, err := db.validateUpdateCollection(ctx, txn, existingDescriptionsByName, desc) if err != nil { return nil, err } @@ -311,17 +312,17 @@ func (db *db) updateCollection( func (db *db) validateUpdateCollection( ctx context.Context, txn datastore.Txn, + existingDescriptionsByName map[string]client.CollectionDescription, proposedDesc client.CollectionDescription, ) (bool, error) { - existingCollection, err := db.getCollectionByName(ctx, txn, proposedDesc.Name) - if err != nil { - if errors.Is(err, ds.ErrNotFound) { - // Original error is quite unhelpful to users at the moment so we return a custom one - return false, NewErrAddCollectionWithPatch(proposedDesc.Name) - } - return false, err + if proposedDesc.Name == "" { + return false, ErrCollectionNameEmpty + } + + existingDesc, collectionExists := existingDescriptionsByName[proposedDesc.Name] + if !collectionExists { + return false, NewErrAddCollectionWithPatch(proposedDesc.Name) } - existingDesc := existingCollection.Description() if proposedDesc.ID != existingDesc.ID { return false, NewErrCollectionIDDoesntMatch(proposedDesc.Name, existingDesc.ID, proposedDesc.ID) diff --git a/db/schema.go b/db/schema.go index e85b0b6a72..34ba35b3a2 100644 --- a/db/schema.go +++ b/db/schema.go @@ -132,7 +132,7 @@ func (db *db) patchSchema(ctx context.Context, txn datastore.Txn, patchString st } for _, desc := range newDescriptions { - if _, err := db.updateCollection(ctx, txn, desc); err != nil { + if _, err := db.updateCollection(ctx, txn, collectionsByName, desc); err != nil { return err } } From 24d00cd88892b91fe94faeac802e714c608d87b2 Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Thu, 3 Aug 2023 14:48:33 -0400 Subject: [PATCH 2/4] Move ErrRelationOneSided to client package Will be used by the db package too shortly --- client/errors.go | 6 ++++++ request/graphql/schema/collection.go | 2 +- request/graphql/schema/errors.go | 6 ------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/errors.go b/client/errors.go index 035ac87235..82203f4047 100644 --- a/client/errors.go +++ b/client/errors.go @@ -22,6 +22,7 @@ const ( errParsingFailed string = "failed to parse argument" errUninitializeProperty string = "invalid state, required property is uninitialized" errMaxTxnRetries string = "reached maximum transaction reties" + errRelationOneSided string = "relation must be defined on both schemas" ) // Errors returnable from this package. @@ -43,6 +44,7 @@ var ( ErrMalformedDocKey = errors.New("malformed DocKey, missing either version or cid") ErrInvalidDocKeyVersion = errors.New("invalid DocKey version") ErrMaxTxnRetries = errors.New(errMaxTxnRetries) + ErrRelationOneSided = errors.New(errRelationOneSided) ) // NewErrFieldNotExist returns an error indicating that the given field does not exist. @@ -97,3 +99,7 @@ func NewErrUninitializeProperty(host string, propertyName string) error { func NewErrMaxTxnRetries(inner error) error { return errors.Wrap(errMaxTxnRetries, inner) } + +func NewErrRelationOneSided(typeName string) error { + return errors.New(errRelationOneSided, errors.NewKV("Type", typeName)) +} diff --git a/request/graphql/schema/collection.go b/request/graphql/schema/collection.go index d48c7bb638..2e7c6bb3e2 100644 --- a/request/graphql/schema/collection.go +++ b/request/graphql/schema/collection.go @@ -443,7 +443,7 @@ func finalizeRelations(relationManager *RelationManager, descriptions []client.C // if not finalized then we are missing one side of the relationship if !rel.finalized { - return NewErrRelationOneSided(field.Schema) + return client.NewErrRelationOneSided(field.Schema) } field.RelationType = rel.Kind() | fieldRelationType diff --git a/request/graphql/schema/errors.go b/request/graphql/schema/errors.go index dd0e3baa63..cf28c7d710 100644 --- a/request/graphql/schema/errors.go +++ b/request/graphql/schema/errors.go @@ -26,7 +26,6 @@ const ( errIndexUnknownArgument string = "index with unknown argument" errIndexInvalidArgument string = "index with invalid argument" errIndexInvalidName string = "index with invalid name" - errRelationOneSided string = "relation must be defined on both schemas" ) var ( @@ -49,7 +48,6 @@ var ( ErrIndexMissingFields = errors.New(errIndexMissingFields) ErrIndexWithUnknownArg = errors.New(errIndexUnknownArgument) ErrIndexWithInvalidArg = errors.New(errIndexInvalidArgument) - ErrRelationOneSided = errors.New(errRelationOneSided) ) func NewErrDuplicateField(objectName, fieldName string) error { @@ -81,10 +79,6 @@ func NewErrRelationMissingField(objectName, fieldName string) error { ) } -func NewErrRelationOneSided(typeName string) error { - return errors.New(errRelationOneSided, errors.NewKV("Type", typeName)) -} - func NewErrAggregateTargetNotFound(objectName, target string) error { return errors.New( errAggregateTargetNotFound, From 229e02a24f904f38edef5086832ffe907ac3be8b Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Thu, 3 Aug 2023 14:51:31 -0400 Subject: [PATCH 3/4] Improve ErrRelationOneSided message --- client/errors.go | 8 ++++++-- request/graphql/schema/collection.go | 2 +- tests/integration/schema/relations_test.go | 6 +++--- tests/integration/schema/simple_test.go | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/client/errors.go b/client/errors.go index 82203f4047..ad1ad0027a 100644 --- a/client/errors.go +++ b/client/errors.go @@ -100,6 +100,10 @@ func NewErrMaxTxnRetries(inner error) error { return errors.Wrap(errMaxTxnRetries, inner) } -func NewErrRelationOneSided(typeName string) error { - return errors.New(errRelationOneSided, errors.NewKV("Type", typeName)) +func NewErrRelationOneSided(fieldName string, typeName string) error { + return errors.New( + errRelationOneSided, + errors.NewKV("Field", fieldName), + errors.NewKV("Type", typeName), + ) } diff --git a/request/graphql/schema/collection.go b/request/graphql/schema/collection.go index 2e7c6bb3e2..00287c4454 100644 --- a/request/graphql/schema/collection.go +++ b/request/graphql/schema/collection.go @@ -443,7 +443,7 @@ func finalizeRelations(relationManager *RelationManager, descriptions []client.C // if not finalized then we are missing one side of the relationship if !rel.finalized { - return client.NewErrRelationOneSided(field.Schema) + return client.NewErrRelationOneSided(field.Name, field.Schema) } field.RelationType = rel.Kind() | fieldRelationType diff --git a/tests/integration/schema/relations_test.go b/tests/integration/schema/relations_test.go index 9af43b2095..d1b420afb6 100644 --- a/tests/integration/schema/relations_test.go +++ b/tests/integration/schema/relations_test.go @@ -135,7 +135,7 @@ func TestSchemaRelationErrorsGivenOneSidedManyRelationField(t *testing.T) { dogs: [Dog] } `, - ExpectedError: "relation must be defined on both schemas. Type: Dog", + ExpectedError: "relation must be defined on both schemas. Field: dogs, Type: Dog", }, }, } @@ -155,7 +155,7 @@ func TestSchemaRelationErrorsGivenOneSidedRelationField(t *testing.T) { dog: Dog } `, - ExpectedError: "relation must be defined on both schemas. Type: Dog", + ExpectedError: "relation must be defined on both schemas. Field: dog, Type: Dog", }, }, } @@ -173,7 +173,7 @@ func TestSchemaRelation_GivenSelfReferemceRelationField_ReturnError(t *testing.T bestMate: Dog } `, - ExpectedError: "relation must be defined on both schemas. Type: Dog", + ExpectedError: "relation must be defined on both schemas. Field: bestMate, Type: Dog", }, }, } diff --git a/tests/integration/schema/simple_test.go b/tests/integration/schema/simple_test.go index c90fee99e0..47ef9810be 100644 --- a/tests/integration/schema/simple_test.go +++ b/tests/integration/schema/simple_test.go @@ -159,7 +159,7 @@ func TestSchemaSimpleErrorsGivenTypeWithInvalidFieldType(t *testing.T) { name: NotAType } `, - ExpectedError: "relation must be defined on both schemas. Type: NotAType", + ExpectedError: "relation must be defined on both schemas. Field: name, Type: NotAType", }, }, } From 0372c5a69c9d3da09e59917bf29b3f63c780093c Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Wed, 2 Aug 2023 12:01:06 -0400 Subject: [PATCH 4/4] Add support for adding relational fields to schema --- db/collection.go | 132 ++++- db/errors.go | 314 +++++++---- db/schema.go | 2 +- .../field/kind/foreign_object_array_test.go | 523 +++++++++++++++++- .../add/field/kind/foreign_object_test.go | 518 ++++++++++++++++- 5 files changed, 1378 insertions(+), 111 deletions(-) diff --git a/db/collection.go b/db/collection.go index 17fbc0e340..dc7f02a2d7 100644 --- a/db/collection.go +++ b/db/collection.go @@ -231,9 +231,10 @@ func (db *db) updateCollection( ctx context.Context, txn datastore.Txn, existingDescriptionsByName map[string]client.CollectionDescription, + proposedDescriptionsByName map[string]client.CollectionDescription, desc client.CollectionDescription, ) (client.Collection, error) { - hasChanged, err := db.validateUpdateCollection(ctx, txn, existingDescriptionsByName, desc) + hasChanged, err := db.validateUpdateCollection(ctx, txn, existingDescriptionsByName, proposedDescriptionsByName, desc) if err != nil { return nil, err } @@ -313,6 +314,7 @@ func (db *db) validateUpdateCollection( ctx context.Context, txn datastore.Txn, existingDescriptionsByName map[string]client.CollectionDescription, + proposedDescriptionsByName map[string]client.CollectionDescription, proposedDesc client.CollectionDescription, ) (bool, error) { if proposedDesc.Name == "" { @@ -347,7 +349,7 @@ func (db *db) validateUpdateCollection( return false, ErrCannotSetVersionID } - hasChangedFields, err := validateUpdateCollectionFields(existingDesc, proposedDesc) + hasChangedFields, err := validateUpdateCollectionFields(proposedDescriptionsByName, existingDesc, proposedDesc) if err != nil { return hasChangedFields, err } @@ -357,6 +359,7 @@ func (db *db) validateUpdateCollection( } func validateUpdateCollectionFields( + descriptionsByName map[string]client.CollectionDescription, existingDesc client.CollectionDescription, proposedDesc client.CollectionDescription, ) (bool, error) { @@ -387,7 +390,130 @@ func validateUpdateCollectionFields( if !fieldAlreadyExists && (proposedField.Kind == client.FieldKind_FOREIGN_OBJECT || proposedField.Kind == client.FieldKind_FOREIGN_OBJECT_ARRAY) { - return false, NewErrCannotAddRelationalField(proposedField.Name, proposedField.Kind) + if proposedField.Schema == "" { + return false, NewErrRelationalFieldMissingSchema(proposedField.Name, proposedField.Kind) + } + + relatedDesc, relatedDescFound := descriptionsByName[proposedField.Schema] + + if !relatedDescFound { + return false, NewErrSchemaNotFound(proposedField.Name, proposedField.Schema) + } + + if proposedField.Kind == client.FieldKind_FOREIGN_OBJECT { + if !proposedField.RelationType.IsSet(client.Relation_Type_ONE) || + !(proposedField.RelationType.IsSet(client.Relation_Type_ONEONE) || + proposedField.RelationType.IsSet(client.Relation_Type_ONEMANY)) { + return false, NewErrRelationalFieldInvalidRelationType( + proposedField.Name, + fmt.Sprintf( + "%v and %v or %v, with optionally %v", + client.Relation_Type_ONE, + client.Relation_Type_ONEONE, + client.Relation_Type_ONEMANY, + client.Relation_Type_Primary, + ), + proposedField.RelationType, + ) + } + } + + if proposedField.Kind == client.FieldKind_FOREIGN_OBJECT_ARRAY { + if !proposedField.RelationType.IsSet(client.Relation_Type_MANY) || + !proposedField.RelationType.IsSet(client.Relation_Type_ONEMANY) { + return false, NewErrRelationalFieldInvalidRelationType( + proposedField.Name, + client.Relation_Type_MANY|client.Relation_Type_ONEMANY, + proposedField.RelationType, + ) + } + } + + if proposedField.RelationName == "" { + return false, NewErrRelationalFieldMissingRelationName(proposedField.Name) + } + + if proposedField.RelationType.IsSet(client.Relation_Type_Primary) { + if proposedField.Kind == client.FieldKind_FOREIGN_OBJECT_ARRAY { + return false, NewErrPrimarySideOnMany(proposedField.Name) + } + + idFieldName := proposedField.Name + "_id" + idField, idFieldFound := proposedDesc.Schema.GetField(idFieldName) + if !idFieldFound { + return false, NewErrRelationalFieldMissingIDField(proposedField.Name, idFieldName) + } + + if idField.Kind != client.FieldKind_DocKey { + return false, NewErrRelationalFieldIDInvalidType(idField.Name, client.FieldKind_DocKey, idField.Kind) + } + + if idField.RelationType != client.Relation_Type_INTERNAL_ID { + return false, NewErrRelationalFieldInvalidRelationType( + idField.Name, + client.Relation_Type_INTERNAL_ID, + idField.RelationType, + ) + } + + if idField.RelationName == "" { + return false, NewErrRelationalFieldMissingRelationName(idField.Name) + } + } + + var relatedFieldFound bool + var relatedField client.FieldDescription + for _, field := range relatedDesc.Schema.Fields { + if field.RelationName == proposedField.RelationName && + !field.RelationType.IsSet(client.Relation_Type_INTERNAL_ID) && + !(relatedDesc.Name == proposedDesc.Name && field.Name == proposedField.Name) { + relatedFieldFound = true + relatedField = field + break + } + } + + if !relatedFieldFound { + return false, client.NewErrRelationOneSided(proposedField.Name, proposedField.Schema) + } + + if !(proposedField.RelationType.IsSet(client.Relation_Type_Primary) || + relatedField.RelationType.IsSet(client.Relation_Type_Primary)) { + return false, NewErrPrimarySideNotDefined(proposedField.RelationName) + } + + if proposedField.RelationType.IsSet(client.Relation_Type_Primary) && + relatedField.RelationType.IsSet(client.Relation_Type_Primary) { + return false, NewErrBothSidesPrimary(proposedField.RelationName) + } + + if proposedField.RelationType.IsSet(client.Relation_Type_ONEONE) && + relatedField.Kind != client.FieldKind_FOREIGN_OBJECT { + return false, NewErrRelatedFieldKindMismatch( + proposedField.RelationName, + client.FieldKind_FOREIGN_OBJECT, + relatedField.Kind, + ) + } + + if proposedField.RelationType.IsSet(client.Relation_Type_ONEMANY) && + proposedField.Kind == client.FieldKind_FOREIGN_OBJECT && + relatedField.Kind != client.FieldKind_FOREIGN_OBJECT_ARRAY { + return false, NewErrRelatedFieldKindMismatch( + proposedField.RelationName, + client.FieldKind_FOREIGN_OBJECT_ARRAY, + relatedField.Kind, + ) + } + + if proposedField.RelationType.IsSet(client.Relation_Type_ONEONE) && + !relatedField.RelationType.IsSet(client.Relation_Type_ONEONE) { + return false, NewErrRelatedFieldRelationTypeMismatch( + proposedField.RelationName, + client.Relation_Type_ONEONE, + relatedField.RelationType, + ) + } } if _, isDuplicate := newFieldNames[proposedField.Name]; isDuplicate { diff --git a/db/errors.go b/db/errors.go index e5b55dcf1a..433e535b20 100644 --- a/db/errors.go +++ b/db/errors.go @@ -16,62 +16,72 @@ import ( ) const ( - errFailedToGetHeads string = "failed to get document heads" - errFailedToCreateCollectionQuery string = "failed to create collection prefix query" - errFailedToGetCollection string = "failed to get collection" - errFailedToGetAllCollections string = "failed to get all collections" - errDocVerification string = "the document verification failed" - errAddingP2PCollection string = "cannot add collection ID" - errRemovingP2PCollection string = "cannot remove collection ID" - errAddCollectionWithPatch string = "unknown collection, adding collections via patch is not supported" - errCollectionIDDoesntMatch string = "CollectionID does not match existing" - errSchemaIDDoesntMatch string = "SchemaID does not match existing" - errCannotModifySchemaName string = "modifying the schema name is not supported" - errCannotSetVersionID string = "setting the VersionID is not supported. It is updated automatically" - errCannotSetFieldID string = "explicitly setting a field ID value is not supported" - errCannotAddRelationalField string = "the adding of new relation fields is not yet supported" - errDuplicateField string = "duplicate field" - errCannotMutateField string = "mutating an existing field is not supported" - 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" - errDocumentAlreadyExists string = "a document with the given dockey already exists" - errDocumentDeleted string = "a document with the given dockey has been deleted" - errIndexMissingFields string = "index missing fields" - errNonZeroIndexIDProvided string = "non-zero index ID provided" - errIndexFieldMissingName string = "index field missing name" - errIndexFieldMissingDirection string = "index field missing direction" - errIndexSingleFieldWrongDirection string = "wrong direction for index with a single field" - errIndexWithNameAlreadyExists string = "index with name already exists" - errInvalidStoredIndex string = "invalid stored index" - errInvalidStoredIndexKey string = "invalid stored index key" - errNonExistingFieldForIndex string = "creating an index on a non-existing property" - errCollectionDoesntExisting string = "collection with given name doesn't exist" - errFailedToStoreIndexedField string = "failed to store indexed field" - errFailedToReadStoredIndexDesc string = "failed to read stored index description" - errCanNotDeleteIndexedField string = "can not delete indexed field" - errCanNotAddIndexWithPatch string = "adding indexes via patch is not supported" - errCanNotDropIndexWithPatch string = "dropping indexes via patch is not supported" - errCanNotChangeIndexWithPatch string = "changing indexes via patch is not supported" - errIndexWithNameDoesNotExists string = "index with name doesn't exists" - errInvalidFieldValue string = "invalid field value" - errUnsupportedIndexFieldType string = "unsupported index field type" - errIndexDescriptionHasNoFields string = "index description has no fields" - errIndexDescHasNonExistingField string = "index description has non existing field" - errFieldOrAliasToFieldNotExist string = "The given field or alias to field does not exist" - errCreateFile string = "failed to create file" - errOpenFile string = "failed to open file" - errCloseFile string = "failed to close file" - errRemoveFile string = "failed to remove file" - errFailedToReadByte string = "failed to read byte" - errFailedToWriteString string = "failed to write string" - errJSONDecode string = "failed to decode JSON" - errDocFromMap string = "failed to create a new doc from map" - errDocCreate string = "failed to save a new doc to collection" - errDocUpdate string = "failed to update doc to collection" - errExpectedJSONObject string = "expected JSON object" - errExpectedJSONArray string = "expected JSON array" + errFailedToGetHeads string = "failed to get document heads" + errFailedToCreateCollectionQuery string = "failed to create collection prefix query" + errFailedToGetCollection string = "failed to get collection" + errFailedToGetAllCollections string = "failed to get all collections" + errDocVerification string = "the document verification failed" + errAddingP2PCollection string = "cannot add collection ID" + errRemovingP2PCollection string = "cannot remove collection ID" + errAddCollectionWithPatch string = "unknown collection, adding collections via patch is not supported" + errCollectionIDDoesntMatch string = "CollectionID does not match existing" + errSchemaIDDoesntMatch string = "SchemaID does not match existing" + errCannotModifySchemaName string = "modifying the schema name is not supported" + errCannotSetVersionID string = "setting the VersionID is not supported. It is updated automatically" + errCannotSetFieldID string = "explicitly setting a field ID value is not supported" + errRelationalFieldMissingSchema string = "a `Schema` [name] must be provided when adding a new relation field" + errRelationalFieldInvalidRelationType string = "invalid RelationType" + errRelationalFieldMissingIDField string = "missing id field for relation object field" + errRelationalFieldMissingRelationName string = "missing relation name" + errPrimarySideNotDefined string = "primary side of relation not defined" + errPrimarySideOnMany string = "cannot set the many side of a relation as primary" + errBothSidesPrimary string = "both sides of a relation cannot be primary" + errRelatedFieldKindMismatch string = "invalid Kind of the related field" + errRelatedFieldRelationTypeMismatch string = "invalid RelationType of the related field" + errRelationalFieldIDInvalidType string = "relational id field of invalid kind" + errDuplicateField string = "duplicate field" + errCannotMutateField string = "mutating an existing field is not supported" + 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" + 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" + errIndexMissingFields string = "index missing fields" + errNonZeroIndexIDProvided string = "non-zero index ID provided" + errIndexFieldMissingName string = "index field missing name" + errIndexFieldMissingDirection string = "index field missing direction" + errIndexSingleFieldWrongDirection string = "wrong direction for index with a single field" + errIndexWithNameAlreadyExists string = "index with name already exists" + errInvalidStoredIndex string = "invalid stored index" + errInvalidStoredIndexKey string = "invalid stored index key" + errNonExistingFieldForIndex string = "creating an index on a non-existing property" + errCollectionDoesntExisting string = "collection with given name doesn't exist" + errFailedToStoreIndexedField string = "failed to store indexed field" + errFailedToReadStoredIndexDesc string = "failed to read stored index description" + errCanNotDeleteIndexedField string = "can not delete indexed field" + errCanNotAddIndexWithPatch string = "adding indexes via patch is not supported" + errCanNotDropIndexWithPatch string = "dropping indexes via patch is not supported" + errCanNotChangeIndexWithPatch string = "changing indexes via patch is not supported" + errIndexWithNameDoesNotExists string = "index with name doesn't exists" + errInvalidFieldValue string = "invalid field value" + errUnsupportedIndexFieldType string = "unsupported index field type" + errIndexDescriptionHasNoFields string = "index description has no fields" + errIndexDescHasNonExistingField string = "index description has non existing field" + errFieldOrAliasToFieldNotExist string = "The given field or alias to field does not exist" + errCreateFile string = "failed to create file" + errOpenFile string = "failed to open file" + errCloseFile string = "failed to close file" + errRemoveFile string = "failed to remove file" + errFailedToReadByte string = "failed to read byte" + errFailedToWriteString string = "failed to write string" + errJSONDecode string = "failed to decode JSON" + errDocFromMap string = "failed to create a new doc from map" + errDocCreate string = "failed to save a new doc to collection" + errDocUpdate string = "failed to update doc to collection" + errExpectedJSONObject string = "expected JSON object" + errExpectedJSONArray string = "expected JSON array" ) var ( @@ -90,53 +100,63 @@ var ( ErrInvalidMergeValueType = errors.New( "the type of value in the merge patch doesn't match the schema", ) - ErrMissingDocFieldToUpdate = errors.New("missing document field to update") - ErrDocMissingKey = errors.New("document is missing key") - ErrInvalidFilter = errors.New("invalid filter") - ErrInvalidOpPath = errors.New("invalid patch op path") - ErrDocumentAlreadyExists = errors.New(errDocumentAlreadyExists) - ErrDocumentDeleted = errors.New(errDocumentDeleted) - ErrUnknownCRDTArgument = errors.New("invalid CRDT arguments") - ErrUnknownCRDT = errors.New("unknown crdt") - ErrSchemaFirstFieldDocKey = errors.New("collection schema first field must be a DocKey") - ErrCollectionAlreadyExists = errors.New("collection already exists") - ErrCollectionNameEmpty = errors.New("collection name can't be empty") - ErrSchemaIDEmpty = errors.New("schema ID can't be empty") - ErrSchemaVersionIDEmpty = errors.New("schema version ID can't be empty") - ErrKeyEmpty = errors.New("key cannot be empty") - ErrAddingP2PCollection = errors.New(errAddingP2PCollection) - ErrRemovingP2PCollection = errors.New(errRemovingP2PCollection) - ErrAddCollectionWithPatch = errors.New(errAddCollectionWithPatch) - ErrCollectionIDDoesntMatch = errors.New(errCollectionIDDoesntMatch) - ErrSchemaIDDoesntMatch = errors.New(errSchemaIDDoesntMatch) - ErrCannotModifySchemaName = errors.New(errCannotModifySchemaName) - ErrCannotSetVersionID = errors.New(errCannotSetVersionID) - ErrCannotSetFieldID = errors.New(errCannotSetFieldID) - ErrCannotAddRelationalField = errors.New(errCannotAddRelationalField) - ErrDuplicateField = errors.New(errDuplicateField) - ErrCannotMutateField = errors.New(errCannotMutateField) - ErrCannotMoveField = errors.New(errCannotMoveField) - ErrInvalidCRDTType = errors.New(errInvalidCRDTType) - ErrCannotDeleteField = errors.New(errCannotDeleteField) - ErrFieldKindNotFound = errors.New(errFieldKindNotFound) - ErrIndexMissingFields = errors.New(errIndexMissingFields) - ErrIndexFieldMissingName = errors.New(errIndexFieldMissingName) - ErrIndexFieldMissingDirection = errors.New(errIndexFieldMissingDirection) - ErrIndexSingleFieldWrongDirection = errors.New(errIndexSingleFieldWrongDirection) - ErrCanNotChangeIndexWithPatch = errors.New(errCanNotChangeIndexWithPatch) - ErrFieldOrAliasToFieldNotExist = errors.New(errFieldOrAliasToFieldNotExist) - ErrCreateFile = errors.New(errCreateFile) - ErrOpenFile = errors.New(errOpenFile) - ErrCloseFile = errors.New(errCloseFile) - ErrRemoveFile = errors.New(errRemoveFile) - ErrFailedToReadByte = errors.New(errFailedToReadByte) - ErrFailedToWriteString = errors.New(errFailedToWriteString) - ErrJSONDecode = errors.New(errJSONDecode) - ErrDocFromMap = errors.New(errDocFromMap) - ErrDocCreate = errors.New(errDocCreate) - ErrDocUpdate = errors.New(errDocUpdate) - ErrExpectedJSONObject = errors.New(errExpectedJSONObject) - ErrExpectedJSONArray = errors.New(errExpectedJSONArray) + ErrMissingDocFieldToUpdate = errors.New("missing document field to update") + ErrDocMissingKey = errors.New("document is missing key") + ErrInvalidFilter = errors.New("invalid filter") + ErrInvalidOpPath = errors.New("invalid patch op path") + ErrDocumentAlreadyExists = errors.New(errDocumentAlreadyExists) + ErrDocumentDeleted = errors.New(errDocumentDeleted) + ErrUnknownCRDTArgument = errors.New("invalid CRDT arguments") + ErrUnknownCRDT = errors.New("unknown crdt") + ErrSchemaFirstFieldDocKey = errors.New("collection schema first field must be a DocKey") + ErrCollectionAlreadyExists = errors.New("collection already exists") + ErrCollectionNameEmpty = errors.New("collection name can't be empty") + ErrSchemaIDEmpty = errors.New("schema ID can't be empty") + ErrSchemaVersionIDEmpty = errors.New("schema version ID can't be empty") + ErrKeyEmpty = errors.New("key cannot be empty") + ErrAddingP2PCollection = errors.New(errAddingP2PCollection) + ErrRemovingP2PCollection = errors.New(errRemovingP2PCollection) + ErrAddCollectionWithPatch = errors.New(errAddCollectionWithPatch) + ErrCollectionIDDoesntMatch = errors.New(errCollectionIDDoesntMatch) + ErrSchemaIDDoesntMatch = errors.New(errSchemaIDDoesntMatch) + ErrCannotModifySchemaName = errors.New(errCannotModifySchemaName) + ErrCannotSetVersionID = errors.New(errCannotSetVersionID) + ErrCannotSetFieldID = errors.New(errCannotSetFieldID) + ErrRelationalFieldMissingSchema = errors.New(errRelationalFieldMissingSchema) + ErrRelationalFieldInvalidRelationType = errors.New(errRelationalFieldInvalidRelationType) + ErrRelationalFieldMissingIDField = errors.New(errRelationalFieldMissingIDField) + ErrRelationalFieldMissingRelationName = errors.New(errRelationalFieldMissingRelationName) + ErrPrimarySideNotDefined = errors.New(errPrimarySideNotDefined) + ErrPrimarySideOnMany = errors.New(errPrimarySideOnMany) + ErrBothSidesPrimary = errors.New(errBothSidesPrimary) + ErrRelatedFieldKindMismatch = errors.New(errRelatedFieldKindMismatch) + ErrRelatedFieldRelationTypeMismatch = errors.New(errRelatedFieldRelationTypeMismatch) + ErrRelationalFieldIDInvalidType = errors.New(errRelationalFieldIDInvalidType) + ErrDuplicateField = errors.New(errDuplicateField) + ErrCannotMutateField = errors.New(errCannotMutateField) + ErrCannotMoveField = errors.New(errCannotMoveField) + ErrInvalidCRDTType = errors.New(errInvalidCRDTType) + ErrCannotDeleteField = errors.New(errCannotDeleteField) + ErrFieldKindNotFound = errors.New(errFieldKindNotFound) + ErrSchemaNotFound = errors.New(errSchemaNotFound) + ErrIndexMissingFields = errors.New(errIndexMissingFields) + ErrIndexFieldMissingName = errors.New(errIndexFieldMissingName) + ErrIndexFieldMissingDirection = errors.New(errIndexFieldMissingDirection) + ErrIndexSingleFieldWrongDirection = errors.New(errIndexSingleFieldWrongDirection) + ErrCanNotChangeIndexWithPatch = errors.New(errCanNotChangeIndexWithPatch) + ErrFieldOrAliasToFieldNotExist = errors.New(errFieldOrAliasToFieldNotExist) + ErrCreateFile = errors.New(errCreateFile) + ErrOpenFile = errors.New(errOpenFile) + ErrCloseFile = errors.New(errCloseFile) + ErrRemoveFile = errors.New(errRemoveFile) + ErrFailedToReadByte = errors.New(errFailedToReadByte) + ErrFailedToWriteString = errors.New(errFailedToWriteString) + ErrJSONDecode = errors.New(errJSONDecode) + ErrDocFromMap = errors.New(errDocFromMap) + ErrDocCreate = errors.New(errDocCreate) + ErrDocUpdate = errors.New(errDocUpdate) + ErrExpectedJSONObject = errors.New(errExpectedJSONObject) + ErrExpectedJSONArray = errors.New(errExpectedJSONArray) ) // NewErrFieldOrAliasToFieldNotExist returns an error indicating that the given field or an alias field does not exist. @@ -276,14 +296,90 @@ func NewErrCannotSetFieldID(name string, id client.FieldID) error { ) } -func NewErrCannotAddRelationalField(name string, kind client.FieldKind) error { +func NewErrRelationalFieldMissingSchema(name string, kind client.FieldKind) error { return errors.New( - errCannotAddRelationalField, + errRelationalFieldMissingSchema, errors.NewKV("Field", name), errors.NewKV("Kind", kind), ) } +func NewErrRelationalFieldInvalidRelationType(name string, expected any, actual client.RelationType) error { + return errors.New( + errRelationalFieldInvalidRelationType, + errors.NewKV("Field", name), + errors.NewKV("Expected", expected), + errors.NewKV("Actual", actual), + ) +} + +func NewErrRelationalFieldMissingIDField(name string, expectedName string) error { + return errors.New( + errRelationalFieldMissingIDField, + errors.NewKV("Field", name), + errors.NewKV("ExpectedIDFieldName", expectedName), + ) +} + +func NewErrRelationalFieldMissingRelationName(name string) error { + return errors.New( + errRelationalFieldMissingRelationName, + errors.NewKV("Field", name), + ) +} + +func NewErrPrimarySideNotDefined(relationName string) error { + return errors.New( + errPrimarySideNotDefined, + errors.NewKV("RelationName", relationName), + ) +} + +func NewErrPrimarySideOnMany(name string) error { + return errors.New( + errPrimarySideOnMany, + errors.NewKV("Field", name), + ) +} + +func NewErrBothSidesPrimary(relationName string) error { + return errors.New( + errBothSidesPrimary, + errors.NewKV("RelationName", relationName), + ) +} + +func NewErrRelatedFieldKindMismatch(relationName string, expected client.FieldKind, actual client.FieldKind) error { + return errors.New( + errRelatedFieldKindMismatch, + errors.NewKV("RelationName", relationName), + errors.NewKV("Expected", expected), + errors.NewKV("Actual", actual), + ) +} + +func NewErrRelatedFieldRelationTypeMismatch( + relationName string, + expected client.RelationType, + actual client.RelationType, +) error { + return errors.New( + errRelatedFieldRelationTypeMismatch, + errors.NewKV("RelationName", relationName), + errors.NewKV("Expected", expected), + errors.NewKV("Actual", actual), + ) +} + +func NewErrRelationalFieldIDInvalidType(name string, expected, actual client.FieldKind) error { + return errors.New( + errRelationalFieldIDInvalidType, + errors.NewKV("Field", name), + errors.NewKV("Expected", expected), + errors.NewKV("Actual", actual), + ) +} + func NewErrFieldKindNotFound(kind string) error { return errors.New( errFieldKindNotFound, @@ -291,6 +387,14 @@ func NewErrFieldKindNotFound(kind string) error { ) } +func NewErrSchemaNotFound(name string, schema string) error { + return errors.New( + errSchemaNotFound, + errors.NewKV("Field", name), + errors.NewKV("Schema", schema), + ) +} + func NewErrDuplicateField(name string) error { return errors.New(errDuplicateField, errors.NewKV("Name", name)) } diff --git a/db/schema.go b/db/schema.go index 34ba35b3a2..5f7dcd6c83 100644 --- a/db/schema.go +++ b/db/schema.go @@ -132,7 +132,7 @@ func (db *db) patchSchema(ctx context.Context, txn datastore.Txn, patchString st } for _, desc := range newDescriptions { - if _, err := db.updateCollection(ctx, txn, collectionsByName, desc); err != nil { + if _, err := db.updateCollection(ctx, txn, collectionsByName, newDescriptionsByName, desc); err != nil { return err } } diff --git a/tests/integration/schema/updates/add/field/kind/foreign_object_array_test.go b/tests/integration/schema/updates/add/field/kind/foreign_object_array_test.go index 6d96324f3c..7743a04cf9 100644 --- a/tests/integration/schema/updates/add/field/kind/foreign_object_array_test.go +++ b/tests/integration/schema/updates/add/field/kind/foreign_object_array_test.go @@ -11,6 +11,7 @@ package kind import ( + "fmt" "testing" testUtils "github.com/sourcenetwork/defradb/tests/integration" @@ -33,7 +34,527 @@ func TestSchemaUpdatesAddFieldKindForeignObjectArray(t *testing.T) { { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo", "Kind": 17} } ] `, - ExpectedError: "the adding of new relation fields is not yet supported. Field: foo, Kind: 17", + ExpectedError: "a `Schema` [name] must be provided when adding a new relation field. Field: foo, Kind: 17", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_InvalidSchemaJson(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), invalid schema json", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo", "Kind": 17, "Schema": 123} } + ] + `, + ExpectedError: "json: cannot unmarshal number into Go struct field FieldDescription.Schema.Fields.Schema of type string", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_MissingRelationType(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (17), missing relation type", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo", "Kind": 17, "Schema": "Users"} } + ] + `, + ExpectedError: "invalid RelationType. Field: foo, Expected: 10, Actual: 0", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_MissingRelationName(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), missing relation name", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 17, "RelationType": 10, "Schema": "Users" + }} + ] + `, + ExpectedError: "missing relation name. Field: foo", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_MissingIDField(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), missing id field", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 137, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + ExpectedError: "missing id field for relation object field. Field: foo, ExpectedIDFieldName: foo_id", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_IDFieldMissingKind(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), id field missing kind", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 137, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo_id"} } + ] + `, + ExpectedError: "relational id field of invalid kind. Field: foo_id, Expected: ID, Actual: 0", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_IDFieldInvalidKind(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), id field invalid kind", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 137, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo_id", "Kind": 2} } + ] + `, + ExpectedError: "relational id field of invalid kind. Field: foo_id, Expected: ID, Actual: Boolean", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_IDFieldMissingRelationType(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), id field missing relation type", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 137, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo_id", "Kind": 1} } + ] + `, + ExpectedError: "invalid RelationType. Field: foo_id, Expected: 64, Actual: 0", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_IDFieldInvalidRelationType(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), id field invalid RelationType", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 137, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo_id", "Kind": 1, "RelationType": 4} } + ] + `, + ExpectedError: "invalid RelationType. Field: foo_id, Expected: 64, Actual: 4", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_IDFieldMissingRelationName(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), id field missing relation name", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 137, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo_id", "Kind": 1, "RelationType": 64} } + ] + `, + ExpectedError: "missing relation name. Field: foo_id", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_OnlyHalfRelationDefined(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), only half relation defined", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 137, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }} + ] + `, + ExpectedError: "relation must be defined on both schemas. Field: foo, Type: Users", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_NoPrimaryDefined(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), no primary defined", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 9, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar", "Kind": 17, "RelationType": 10, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + ExpectedError: "primary side of relation not defined. RelationName: foo", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_PrimaryDefinedOnManySide(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), no primary defined", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 9, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar", "Kind": 17, "RelationType": 138, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + ExpectedError: "cannot set the many side of a relation as primary. Field: foobar", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_RelatedKindMismatch(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), related kind mismatch", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 137, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar", "Kind": 16, "RelationType": 10, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + ExpectedError: "invalid Kind of the related field. RelationName: foo, Expected: 17, Actual: 16", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_RelatedKindAndRelationTypeMismatch(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), related kind mismatch", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 137, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar", "Kind": 16, "RelationType": 9, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + ExpectedError: "invalid Kind of the related field. RelationName: foo, Expected: 17, Actual: 16", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_RelatedRelationTypeMismatch(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), related relation type mismatch", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 137, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar", "Kind": 16, "RelationType": 5, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + ExpectedError: "invalid Kind of the related field. RelationName: foo, Expected: 17, Actual: 16", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObjectArray_Succeeds(t *testing.T) { + key1 := "bae-decf6467-4c7c-50d7-b09d-0a7097ef6bad" + + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17), valid, functional", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 137, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar", "Kind": 17, "RelationType": 10, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + }, + testUtils.Request{ + Request: `mutation { + create_Users(data: "{\"name\": \"John\"}") { + _key + } + }`, + Results: []map[string]any{ + { + "_key": key1, + }, + }, + }, + testUtils.Request{ + Request: fmt.Sprintf(`mutation { + create_Users(data: "{\"name\": \"Keenan\", \"foo\": \"%s\"}") { + name + foo { + name + } + } + }`, + key1, + ), + Results: []map[string]any{ + { + "name": "Keenan", + "foo": map[string]any{ + "name": "John", + }, + }, + }, + }, + testUtils.Request{ + Request: `query { + Users { + name + foo { + name + } + foobar { + name + } + } + }`, + Results: []map[string]any{ + { + "name": "Keenan", + "foo": map[string]any{ + "name": "John", + }, + "foobar": []map[string]any{}, + }, + { + "name": "John", + "foo": nil, + "foobar": []map[string]any{ + { + "name": "Keenan", + }, + }, + }, + }, }, }, } diff --git a/tests/integration/schema/updates/add/field/kind/foreign_object_test.go b/tests/integration/schema/updates/add/field/kind/foreign_object_test.go index 76dd134982..e899092603 100644 --- a/tests/integration/schema/updates/add/field/kind/foreign_object_test.go +++ b/tests/integration/schema/updates/add/field/kind/foreign_object_test.go @@ -11,6 +11,7 @@ package kind import ( + "fmt" "testing" testUtils "github.com/sourcenetwork/defradb/tests/integration" @@ -33,7 +34,522 @@ func TestSchemaUpdatesAddFieldKindForeignObject(t *testing.T) { { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo", "Kind": 16} } ] `, - ExpectedError: "the adding of new relation fields is not yet supported. Field: foo, Kind: 16", + ExpectedError: "a `Schema` [name] must be provided when adding a new relation field. Field: foo, Kind: 16", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_InvalidSchemaJson(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), invalid schema json", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo", "Kind": 16, "Schema": 123} } + ] + `, + ExpectedError: "json: cannot unmarshal number into Go struct field FieldDescription.Schema.Fields.Schema of type string", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_MissingRelationType(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), missing relation type", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo", "Kind": 16, "Schema": "Users"} } + ] + `, + ExpectedError: "invalid RelationType. Field: foo, Expected: 1 and 4 or 8, with optionally 128, Actual: 0", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_UnknownSchema(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), unknown schema", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 5, "Schema": "Unknown" + }} + ] + `, + ExpectedError: "no schema found for given name. Field: foo, Schema: Unknown", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_MissingRelationName(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), missing relation name", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 5, "Schema": "Users" + }} + ] + `, + ExpectedError: "missing relation name. Field: foo", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_MissingIDField(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), missing id field", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + ExpectedError: "missing id field for relation object field. Field: foo, ExpectedIDFieldName: foo_id", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_IDFieldMissingKind(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), id field missing kind", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo_id"} } + ] + `, + ExpectedError: "relational id field of invalid kind. Field: foo_id, Expected: ID, Actual: 0", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_IDFieldInvalidKind(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), id field invalid kind", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo_id", "Kind": 2} } + ] + `, + ExpectedError: "relational id field of invalid kind. Field: foo_id, Expected: ID, Actual: Boolean", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_IDFieldMissingRelationType(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), id field missing relation type", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo_id", "Kind": 1} } + ] + `, + ExpectedError: "invalid RelationType. Field: foo_id, Expected: 64, Actual: 0", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_IDFieldInvalidRelationType(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), id field invalid RelationType", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo_id", "Kind": 1, "RelationType": 4} } + ] + `, + ExpectedError: "invalid RelationType. Field: foo_id, Expected: 64, Actual: 4", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_IDFieldMissingRelationName(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), id field missing relation name", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "foo_id", "Kind": 1, "RelationType": 64} } + ] + `, + ExpectedError: "missing relation name. Field: foo_id", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_OnlyHalfRelationDefined(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), only half relation defined", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }} + ] + `, + ExpectedError: "relation must be defined on both schemas. Field: foo, Type: Users", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_NoPrimaryDefined(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), no primary defined", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 5, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar", "Kind": 16, "RelationType": 5, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + ExpectedError: "primary side of relation not defined. RelationName: foo", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_BothSidesPrimary(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), both sides primary", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar_id", "Kind": 1, "RelationType": 64, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + ExpectedError: "both sides of a relation cannot be primary. RelationName: foo", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_RelatedKindMismatch(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), related kind mismatch", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar", "Kind": 17, "RelationType": 5, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + ExpectedError: "invalid Kind of the related field. RelationName: foo, Expected: 16, Actual: 17", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_RelatedRelationTypeMismatch(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), related relation type mismatch", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar", "Kind": 16, "RelationType": 9, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + ExpectedError: "invalid RelationType of the related field. RelationName: foo, Expected: 4, Actual: 9", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaUpdatesAddFieldKindForeignObject_Succeeds(t *testing.T) { + key1 := "bae-decf6467-4c7c-50d7-b09d-0a7097ef6bad" + + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16), valid, functional", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo", "Kind": 16, "RelationType": 133, "Schema": "Users", "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foo_id", "Kind": 1, "RelationType": 64, "RelationName": "foo" + }}, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": { + "Name": "foobar", "Kind": 16, "RelationType": 5, "Schema": "Users", "RelationName": "foo" + }} + ] + `, + }, + testUtils.Request{ + Request: `mutation { + create_Users(data: "{\"name\": \"John\"}") { + _key + } + }`, + Results: []map[string]any{ + { + "_key": key1, + }, + }, + }, + testUtils.Request{ + Request: fmt.Sprintf(`mutation { + create_Users(data: "{\"name\": \"Keenan\", \"foo\": \"%s\"}") { + name + foo { + name + } + } + }`, + key1, + ), + Results: []map[string]any{ + { + "name": "Keenan", + "foo": map[string]any{ + "name": "John", + }, + }, + }, + }, + testUtils.Request{ + Request: `query { + Users { + name + foo { + name + } + foobar { + name + } + } + }`, + Results: []map[string]any{ + { + "name": "Keenan", + "foo": map[string]any{ + "name": "John", + }, + "foobar": nil, + }, + { + "name": "John", + "foo": nil, + "foobar": map[string]any{ + "name": "Keenan", + }, + }, + }, }, }, }