From eb7c28b8498d90b7fc6e582818eebe751b7ae16c Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Fri, 24 Feb 2023 15:41:08 -0500 Subject: [PATCH 01/12] Correct P2P error message Spotted and quickly corrected. Has nothing to do with this PR though, but is small enough to include. --- db/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/errors.go b/db/errors.go index 2becd9c527..1307fd0b47 100644 --- a/db/errors.go +++ b/db/errors.go @@ -20,7 +20,7 @@ const ( errFailedToGetCollection string = "failed to get collection" errDocVerification string = "the document verification failed" errAddingP2PCollection string = "cannot add collection ID" - errRemovingP2PCollection string = "cannot add collection ID" + errRemovingP2PCollection string = "cannot remove collection ID" ) var ( From e71a52d71a8d22d5cfa19b6a296088af0964f5a7 Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Mon, 20 Feb 2023 16:23:36 -0500 Subject: [PATCH 02/12] Remove field kind decimal It is not supported and does not work. Leaving it here will just confuse users, especially as they start to use the schema update system. --- client/descriptions.go | 2 +- db/collection_update.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/descriptions.go b/client/descriptions.go index 4220a332d0..ef6432f866 100644 --- a/client/descriptions.go +++ b/client/descriptions.go @@ -136,7 +136,7 @@ const ( FieldKind_INT_ARRAY FieldKind = 5 FieldKind_FLOAT FieldKind = 6 FieldKind_FLOAT_ARRAY FieldKind = 7 - FieldKind_DECIMAL FieldKind = 8 + _ FieldKind = 8 // safe to repurpose (was never used) _ FieldKind = 9 // safe to repurpose (previoulsy old field) FieldKind_DATETIME FieldKind = 10 FieldKind_STRING FieldKind = 11 diff --git a/db/collection_update.go b/db/collection_update.go index 2a0be023c3..913e7d8b08 100644 --- a/db/collection_update.go +++ b/db/collection_update.go @@ -394,7 +394,7 @@ func validateFieldSchema(val *fastjson.Value, field client.FieldDescription) (an case client.FieldKind_NILLABLE_BOOL_ARRAY: return getNillableArray(val, getBool) - case client.FieldKind_FLOAT, client.FieldKind_DECIMAL: + case client.FieldKind_FLOAT: return getFloat64(val) case client.FieldKind_FLOAT_ARRAY: From 5eb5495a56b58100759672e35abee9ac9ee99f08 Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Mon, 20 Feb 2023 16:42:11 -0500 Subject: [PATCH 03/12] Remove field kind bytes It is not supported and does not work. Leaving it here will just confuse users, especially as they start to use the schema update system. --- client/descriptions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/descriptions.go b/client/descriptions.go index ef6432f866..171091cb06 100644 --- a/client/descriptions.go +++ b/client/descriptions.go @@ -141,7 +141,7 @@ const ( FieldKind_DATETIME FieldKind = 10 FieldKind_STRING FieldKind = 11 FieldKind_STRING_ARRAY FieldKind = 12 - FieldKind_BYTES FieldKind = 13 + _ FieldKind = 13 // safe to repurpose (was never used) // Embedded object within the type FieldKind_OBJECT FieldKind = 14 From 4377581d4c9517946234931c37c8e639253e29d5 Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Mon, 20 Feb 2023 16:51:29 -0500 Subject: [PATCH 04/12] Remove field embedded object kinds They are not supported and does not work. Leaving it here will just confuse users, especially as they start to use the schema update system. --- client/descriptions.go | 10 +++------- db/collection_update.go | 3 +-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/client/descriptions.go b/client/descriptions.go index 171091cb06..1b45df9e1a 100644 --- a/client/descriptions.go +++ b/client/descriptions.go @@ -142,12 +142,8 @@ const ( FieldKind_STRING FieldKind = 11 FieldKind_STRING_ARRAY FieldKind = 12 _ FieldKind = 13 // safe to repurpose (was never used) - - // Embedded object within the type - FieldKind_OBJECT FieldKind = 14 - - // Array of embedded objects - FieldKind_OBJECT_ARRAY FieldKind = 15 + _ FieldKind = 14 // safe to repurpose (was never used) + _ FieldKind = 15 // safe to repurpose (was never used) // Embedded object, but accessed via foreign keys FieldKind_FOREIGN_OBJECT FieldKind = 16 @@ -201,7 +197,7 @@ type FieldDescription struct { // IsObject returns true if this field is an object type. func (f FieldDescription) IsObject() bool { - return (f.Kind == FieldKind_OBJECT) || (f.Kind == FieldKind_FOREIGN_OBJECT) || + return (f.Kind == FieldKind_FOREIGN_OBJECT) || (f.Kind == FieldKind_FOREIGN_OBJECT_ARRAY) } diff --git a/db/collection_update.go b/db/collection_update.go index 913e7d8b08..2ac5cfb4d4 100644 --- a/db/collection_update.go +++ b/db/collection_update.go @@ -420,8 +420,7 @@ func validateFieldSchema(val *fastjson.Value, field client.FieldDescription) (an case client.FieldKind_NILLABLE_INT_ARRAY: return getNillableArray(val, getInt64) - case client.FieldKind_OBJECT, client.FieldKind_OBJECT_ARRAY, - client.FieldKind_FOREIGN_OBJECT, client.FieldKind_FOREIGN_OBJECT_ARRAY: + case client.FieldKind_FOREIGN_OBJECT, client.FieldKind_FOREIGN_OBJECT_ARRAY: return nil, ErrMergeSubTypeNotSupported } From 05d5078accce4f020fa18ca9988c244d0d0de47e Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Fri, 24 Feb 2023 16:29:02 -0500 Subject: [PATCH 05/12] Add documentation to FieldDesc.Kind I'm not sure any more than this is really needed, especially given that we will largely be hiding this from users shortly after merge. --- client/descriptions.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/descriptions.go b/client/descriptions.go index 1b45df9e1a..4b80476a00 100644 --- a/client/descriptions.go +++ b/client/descriptions.go @@ -181,8 +181,12 @@ func (f FieldID) String() string { // FieldDescription describes a field on a Schema and its associated metadata. type FieldDescription struct { - Name string - ID FieldID + Name string + ID FieldID + + // The data type that this field holds. + // + // Must contain a valid value. Kind FieldKind Schema string // If the field is an OBJECT type, then it has a target schema RelationName string // The name of the relation index if the field is of type FOREIGN_OBJECT From 362d204c8f0bd73f3b368e85344dfa7fee2b3a0e Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Thu, 16 Feb 2023 15:25:11 -0500 Subject: [PATCH 06/12] Assert for collection name uniquenss within SDL Previously this would partially succede - the gql types would be updated, but the saving of the collection descriptions would fail (as there is another uniqueness check there), leaving the database in an invalid state until restart. --- request/graphql/schema/generate.go | 3 --- tests/integration/schema/simple_test.go | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/request/graphql/schema/generate.go b/request/graphql/schema/generate.go index 363b1adfcb..3809b98134 100644 --- a/request/graphql/schema/generate.go +++ b/request/graphql/schema/generate.go @@ -421,10 +421,7 @@ func (g *Generator) buildTypes( obj := gql.NewObject(objconf) objs = append(objs, obj) - } - // add all the new types now that they're converted to gql.Objects - for _, obj := range objs { g.manager.schema.TypeMap()[obj.Name()] = obj g.typeDefs = append(g.typeDefs, obj) } diff --git a/tests/integration/schema/simple_test.go b/tests/integration/schema/simple_test.go index 912771784d..e67cfd5719 100644 --- a/tests/integration/schema/simple_test.go +++ b/tests/integration/schema/simple_test.go @@ -61,6 +61,20 @@ func TestSchemaSimpleErrorsGivenDuplicateSchema(t *testing.T) { ExecuteRequestTestCase(t, test) } +func TestSchemaSimpleErrorsGivenDuplicateSchemaInSameSDL(t *testing.T) { + test := RequestTestCase{ + Schema: []string{ + ` + type users {} + type users {} + `, + }, + ExpectedError: "schema type already exists", + } + + ExecuteRequestTestCase(t, test) +} + func TestSchemaSimpleCreatesSchemaGivenNewTypes(t *testing.T) { test := RequestTestCase{ Schema: []string{ From 7f6f92d4de5b6dae514458bc4aa6c51169cdef6b Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Fri, 17 Feb 2023 16:43:12 -0500 Subject: [PATCH 07/12] Correctly query all existing collections Was incorrectly quering the version history, returning a collection per version, instead of just the current version. Went unnoticed as previously each collection could only have a single version. --- db/collection.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/db/collection.go b/db/collection.go index 08323538fc..08b6390c02 100644 --- a/db/collection.go +++ b/db/collection.go @@ -245,10 +245,9 @@ func (db *db) GetCollectionBySchemaID( // GetAllCollections gets all the currently defined collections. func (db *db) GetAllCollections(ctx context.Context) ([]client.Collection, error) { // create collection system prefix query - prefix := core.NewCollectionSchemaVersionKey("") + prefix := core.NewCollectionKey("") q, err := db.systemstore().Query(ctx, query.Query{ - Prefix: prefix.ToString(), - KeysOnly: true, + Prefix: prefix.ToString(), }) if err != nil { return nil, NewErrFailedToCreateCollectionQuery(err) @@ -265,7 +264,7 @@ func (db *db) GetAllCollections(ctx context.Context) ([]client.Collection, error return nil, err } - schemaVersionId := ds.NewKey(res.Key).BaseNamespace() + schemaVersionId := string(res.Value) col, err := db.getCollectionByVersionId(ctx, schemaVersionId) if err != nil { return nil, NewErrFailedToGetCollection(schemaVersionId, err) From 4b5d326f1b3537789e0b857daa76ce8a09cacaad Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Thu, 16 Feb 2023 16:01:10 -0500 Subject: [PATCH 08/12] Make collection persistance transactional Collection stuff needs to be protected by transactions. Partial success of either a create or a mutate cannot be permitted and the use of transactions protects against this. The transactions are also needed to protect against the use of stale data, by including the collection in the transaction used for P2P and query/planner stuff we should ensure that stuff is done against a single, complete version of the collection, and that it is not possible for the collection to mutate whilst something is using the collection. At the moment such concurrent use should now result in a transaction conflict error - this is not ideal and the logic here should probably grow to permit the queuing of such clashes (e.g. through use of a mutex) instead of making the users retry until it succedes - such a change will very likely need to be done within the scope declared by the transaction anyway, so I see no wasted code/time by the changes in this commit here - it is a start, and prevents odd/damaging stuff from happening. DB init was also covered almost as a side-affect, as the sequence needed protecting, and TBH it is probably a very good thing to protect against mutating the database state in the case of a failed init. --- client/db.go | 6 ++- client/p2p.go | 16 +++++- db/collection.go | 113 ++++++++++++++++++++++++++++++++++++------- db/db.go | 24 +++++++-- db/p2p_collection.go | 45 +++++++++++++++-- db/schema.go | 15 ++++-- db/sequence.go | 23 +++++---- net/peer.go | 83 ++++++++++++++++++++++++++----- net/server.go | 9 ++-- planner/create.go | 2 +- planner/delete.go | 2 +- planner/update.go | 2 +- 12 files changed, 278 insertions(+), 62 deletions(-) diff --git a/client/db.go b/client/db.go index 02fee282bd..ea32ec3f6f 100644 --- a/client/db.go +++ b/client/db.go @@ -23,9 +23,13 @@ type DB interface { AddSchema(context.Context, string) error CreateCollection(context.Context, CollectionDescription) (Collection, error) + CreateCollectionTxn(context.Context, datastore.Txn, CollectionDescription) (Collection, error) GetCollectionByName(context.Context, string) (Collection, error) + GetCollectionByNameTxn(context.Context, datastore.Txn, string) (Collection, error) GetCollectionBySchemaID(context.Context, string) (Collection, error) - GetAllCollections(ctx context.Context) ([]Collection, error) + GetCollectionBySchemaIDTxn(context.Context, datastore.Txn, string) (Collection, error) + GetAllCollections(context.Context) ([]Collection, error) + GetAllCollectionsTxn(context.Context, datastore.Txn) ([]Collection, error) Root() datastore.RootStore Blockstore() blockstore.Blockstore diff --git a/client/p2p.go b/client/p2p.go index 33b499e93b..03643b4527 100644 --- a/client/p2p.go +++ b/client/p2p.go @@ -10,7 +10,11 @@ package client -import "context" +import ( + "context" + + "github.com/sourcenetwork/defradb/datastore" +) type P2P interface { // SetReplicator adds a replicator to the persisted list or adds @@ -27,10 +31,20 @@ type P2P interface { // subscribes to to the the persisted list. It will error if the provided // collection ID is invalid. AddP2PCollection(ctx context.Context, collectionID string) error + // AddP2PCollectionTxn adds the given collection ID that the P2P system + // subscribes to to the the persisted list. It will error if the provided + // collection ID is invalid. + AddP2PCollectionTxn(ctx context.Context, txn datastore.Txn, collectionID string) error + // RemoveP2PCollection removes the given collection ID that the P2P system // subscribes to from the the persisted list. It will error if the provided // collection ID is invalid. RemoveP2PCollection(ctx context.Context, collectionID string) error + // RemoveP2PCollectionTxn removes the given collection ID that the P2P system + // subscribes to from the the persisted list. It will error if the provided + // collection ID is invalid. + RemoveP2PCollectionTxn(ctx context.Context, txn datastore.Txn, collectionID string) error + // GetAllP2PCollections returns the list of persisted collection IDs that // the P2P system subscribes to. GetAllP2PCollections(ctx context.Context) ([]string, error) diff --git a/db/collection.go b/db/collection.go index 08b6390c02..124b3f2627 100644 --- a/db/collection.go +++ b/db/collection.go @@ -91,15 +91,35 @@ func (db *db) newCollection(desc client.CollectionDescription) (*collection, err }, nil } -// CreateCollection creates a collection and saves it to the database in its system store. -// Note: Collection.ID is an autoincrementing value that is generated by the database. func (db *db) CreateCollection( ctx context.Context, desc client.CollectionDescription, +) (client.Collection, error) { + txn, err := db.NewTxn(ctx, false) + if err != nil { + return nil, err + } + defer txn.Discard(ctx) + + col, err := db.CreateCollectionTxn(ctx, txn, desc) + if err != nil { + return nil, err + } + + err = txn.Commit(ctx) + return col, err +} + +// CreateCollectionTxn creates a collection and saves it to the database in its system store. +// Note: Collection.ID is an autoincrementing value that is generated by the database. +func (db *db) CreateCollectionTxn( + ctx context.Context, + txn datastore.Txn, + desc client.CollectionDescription, ) (client.Collection, error) { // check if collection by this name exists collectionKey := core.NewCollectionKey(desc.Name) - exists, err := db.systemstore().Has(ctx, collectionKey.ToDS()) + exists, err := txn.Systemstore().Has(ctx, collectionKey.ToDS()) if err != nil { return nil, err } @@ -107,11 +127,11 @@ func (db *db) CreateCollection( return nil, ErrCollectionAlreadyExists } - colSeq, err := db.getSequence(ctx, core.COLLECTION) + colSeq, err := db.getSequence(ctx, txn, core.COLLECTION) if err != nil { return nil, err } - colID, err := colSeq.next(ctx) + colID, err := colSeq.next(ctx, txn) if err != nil { return nil, err } @@ -154,18 +174,18 @@ func (db *db) CreateCollection( collectionSchemaVersionKey := core.NewCollectionSchemaVersionKey(schemaVersionID) // Whilst the schemaVersionKey is global, the data persisted at the key's location // is local to the node (the global only elements are not useful beyond key generation). - err = db.systemstore().Put(ctx, collectionSchemaVersionKey.ToDS(), buf) + err = txn.Systemstore().Put(ctx, collectionSchemaVersionKey.ToDS(), buf) if err != nil { return nil, err } collectionSchemaKey := core.NewCollectionSchemaKey(schemaID) - err = db.systemstore().Put(ctx, collectionSchemaKey.ToDS(), []byte(schemaVersionID)) + err = txn.Systemstore().Put(ctx, collectionSchemaKey.ToDS(), []byte(schemaVersionID)) if err != nil { return nil, err } - err = db.systemstore().Put(ctx, collectionKey.ToDS(), []byte(schemaVersionID)) + err = txn.Systemstore().Put(ctx, collectionKey.ToDS(), []byte(schemaVersionID)) if err != nil { return nil, err } @@ -182,13 +202,17 @@ func (db *db) CreateCollection( // getCollectionByVersionId returns the [*collection] at the given [schemaVersionId] version. // // Will return an error if the given key is empty, or not found. -func (db *db) getCollectionByVersionId(ctx context.Context, schemaVersionId string) (*collection, error) { +func (db *db) getCollectionByVersionId( + ctx context.Context, + txn datastore.Txn, + schemaVersionId string, +) (*collection, error) { if schemaVersionId == "" { return nil, ErrSchemaVersionIdEmpty } key := core.NewCollectionSchemaVersionKey(schemaVersionId) - buf, err := db.systemstore().Get(ctx, key.ToDS()) + buf, err := txn.Systemstore().Get(ctx, key.ToDS()) if err != nil { return nil, err } @@ -207,46 +231,101 @@ func (db *db) getCollectionByVersionId(ctx context.Context, schemaVersionId stri }, nil } -// GetCollection returns an existing collection within the database. +// GetCollectionByName returns an existing collection within the database. func (db *db) GetCollectionByName(ctx context.Context, name string) (client.Collection, error) { + txn, err := db.NewTxn(ctx, true) + if err != nil { + return nil, err + } + defer txn.Discard(ctx) + + col, err := db.GetCollectionByNameTxn(ctx, txn, name) + if err != nil { + return nil, err + } + + err = txn.Commit(ctx) + return col, err +} + +// GetCollectionByNameTxn returns an existing collection within the database. +func (db *db) GetCollectionByNameTxn(ctx context.Context, txn datastore.Txn, name string) (client.Collection, error) { if name == "" { return nil, ErrCollectionNameEmpty } key := core.NewCollectionKey(name) - buf, err := db.systemstore().Get(ctx, key.ToDS()) + buf, err := txn.Systemstore().Get(ctx, key.ToDS()) if err != nil { return nil, err } schemaVersionId := string(buf) - return db.getCollectionByVersionId(ctx, schemaVersionId) + return db.getCollectionByVersionId(ctx, txn, schemaVersionId) } // GetCollectionBySchemaID returns an existing collection using the schema hash ID. func (db *db) GetCollectionBySchemaID( ctx context.Context, schemaID string, +) (client.Collection, error) { + txn, err := db.NewTxn(ctx, true) + if err != nil { + return nil, err + } + defer txn.Discard(ctx) + + col, err := db.GetCollectionBySchemaIDTxn(ctx, txn, schemaID) + if err != nil { + return nil, err + } + + err = txn.Commit(ctx) + return col, err +} + +// GetCollectionBySchemaIDTxn returns an existing collection using the schema hash ID. +func (db *db) GetCollectionBySchemaIDTxn( + ctx context.Context, + txn datastore.Txn, + schemaID string, ) (client.Collection, error) { if schemaID == "" { return nil, ErrSchemaIdEmpty } key := core.NewCollectionSchemaKey(schemaID) - buf, err := db.systemstore().Get(ctx, key.ToDS()) + buf, err := txn.Systemstore().Get(ctx, key.ToDS()) if err != nil { return nil, err } schemaVersionId := string(buf) - return db.getCollectionByVersionId(ctx, schemaVersionId) + return db.getCollectionByVersionId(ctx, txn, schemaVersionId) } // GetAllCollections gets all the currently defined collections. func (db *db) GetAllCollections(ctx context.Context) ([]client.Collection, error) { + txn, err := db.NewTxn(ctx, true) + if err != nil { + return nil, err + } + defer txn.Discard(ctx) + + col, err := db.GetAllCollectionsTxn(ctx, txn) + if err != nil { + return nil, err + } + + err = txn.Commit(ctx) + return col, err +} + +// GetAllCollectionsTxn gets all the currently defined collections. +func (db *db) GetAllCollectionsTxn(ctx context.Context, txn datastore.Txn) ([]client.Collection, error) { // create collection system prefix query prefix := core.NewCollectionKey("") - q, err := db.systemstore().Query(ctx, query.Query{ + q, err := txn.Systemstore().Query(ctx, query.Query{ Prefix: prefix.ToString(), }) if err != nil { @@ -265,7 +344,7 @@ func (db *db) GetAllCollections(ctx context.Context) ([]client.Collection, error } schemaVersionId := string(res.Value) - col, err := db.getCollectionByVersionId(ctx, schemaVersionId) + col, err := db.getCollectionByVersionId(ctx, txn, schemaVersionId) if err != nil { return nil, NewErrFailedToGetCollection(schemaVersionId, err) } diff --git a/db/db.go b/db/db.go index 7954e375df..301ac92aff 100644 --- a/db/db.go +++ b/db/db.go @@ -161,8 +161,14 @@ func (db *db) initialize(ctx context.Context) error { db.glock.Lock() defer db.glock.Unlock() + txn, err := db.NewTxn(ctx, false) + if err != nil { + return err + } + defer txn.Discard(ctx) + log.Debug(ctx, "Checking if DB has already been initialized...") - exists, err := db.systemstore().Has(ctx, ds.NewKey("init")) + exists, err := txn.Systemstore().Has(ctx, ds.NewKey("init")) if err != nil && !errors.Is(err, ds.ErrNotFound) { return err } @@ -170,23 +176,31 @@ func (db *db) initialize(ctx context.Context) error { // and finish initialization if exists { log.Debug(ctx, "DB has already been initialized, continuing") - return db.loadSchema(ctx) + err = db.loadSchema(ctx, txn) + if err != nil { + return err + } + // The query language types are only updated on successful commit + // so we must not forget to do so on success regardless of whether + // we have written to the datastores. + return txn.Commit(ctx) } log.Debug(ctx, "Opened a new DB, needs full initialization") + // init meta data // collection sequence - _, err = db.getSequence(ctx, core.COLLECTION) + _, err = db.getSequence(ctx, txn, core.COLLECTION) if err != nil { return err } - err = db.systemstore().Put(ctx, ds.NewKey("init"), []byte{1}) + err = txn.Systemstore().Put(ctx, ds.NewKey("init"), []byte{1}) if err != nil { return err } - return nil + return txn.Commit(ctx) } // Events returns the events Channel. diff --git a/db/p2p_collection.go b/db/p2p_collection.go index a5aa795d9d..ec8c439d81 100644 --- a/db/p2p_collection.go +++ b/db/p2p_collection.go @@ -16,6 +16,7 @@ import ( dsq "github.com/ipfs/go-datastore/query" "github.com/sourcenetwork/defradb/core" + "github.com/sourcenetwork/defradb/datastore" ) const marker = byte(0xff) @@ -24,24 +25,60 @@ const marker = byte(0xff) // subscribes to to the the persisted list. It will error if the provided // collection ID is invalid. func (db *db) AddP2PCollection(ctx context.Context, collectionID string) error { - _, err := db.GetCollectionBySchemaID(ctx, collectionID) + txn, err := db.NewTxn(ctx, false) + if err != nil { + return err + } + defer txn.Discard(ctx) + + err = db.AddP2PCollectionTxn(ctx, txn, collectionID) + if err != nil { + return err + } + + return txn.Commit(ctx) +} + +// AddP2PCollectionTxn adds the given collection ID that the P2P system +// subscribes to to the the persisted list. It will error if the provided +// collection ID is invalid. +func (db *db) AddP2PCollectionTxn(ctx context.Context, txn datastore.Txn, collectionID string) error { + _, err := db.GetCollectionBySchemaIDTxn(ctx, txn, collectionID) if err != nil { return NewErrAddingP2PCollection(err) } key := core.NewP2PCollectionKey(collectionID) - return db.systemstore().Put(ctx, key.ToDS(), []byte{marker}) + return txn.Systemstore().Put(ctx, key.ToDS(), []byte{marker}) } // RemoveP2PCollection removes the given collection ID that the P2P system // subscribes to from the the persisted list. It will error if the provided // collection ID is invalid. func (db *db) RemoveP2PCollection(ctx context.Context, collectionID string) error { - _, err := db.GetCollectionBySchemaID(ctx, collectionID) + txn, err := db.NewTxn(ctx, false) + if err != nil { + return err + } + defer txn.Discard(ctx) + + err = db.RemoveP2PCollectionTxn(ctx, txn, collectionID) + if err != nil { + return err + } + + return txn.Commit(ctx) +} + +// RemoveP2PCollectionTxn removes the given collection ID that the P2P system +// subscribes to from the the persisted list. It will error if the provided +// collection ID is invalid. +func (db *db) RemoveP2PCollectionTxn(ctx context.Context, txn datastore.Txn, collectionID string) error { + _, err := db.GetCollectionBySchemaIDTxn(ctx, txn, collectionID) if err != nil { return NewErrRemovingP2PCollection(err) } key := core.NewP2PCollectionKey(collectionID) - return db.systemstore().Delete(ctx, key.ToDS()) + return txn.Systemstore().Delete(ctx, key.ToDS()) } // GetAllP2PCollections returns the list of persisted collection IDs that diff --git a/db/schema.go b/db/schema.go index e00eedf3ac..aeec87c73e 100644 --- a/db/schema.go +++ b/db/schema.go @@ -14,11 +14,18 @@ import ( "context" "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/datastore" ) // AddSchema takes the provided schema in SDL format, and applies it to the database, // and creates the necessary collections, request types, etc. func (db *db) AddSchema(ctx context.Context, schemaString string) error { + txn, err := db.NewTxn(ctx, false) + if err != nil { + return err + } + defer txn.Discard(ctx) + collectionDescriptions, err := db.parser.ParseSDL(ctx, schemaString) if err != nil { return err @@ -30,16 +37,16 @@ func (db *db) AddSchema(ctx context.Context, schemaString string) error { } for _, desc := range collectionDescriptions { - if _, err := db.CreateCollection(ctx, desc); err != nil { + if _, err := db.CreateCollectionTxn(ctx, txn, desc); err != nil { return err } } - return nil + return txn.Commit(ctx) } -func (db *db) loadSchema(ctx context.Context) error { - collections, err := db.GetAllCollections(ctx) +func (db *db) loadSchema(ctx context.Context, txn datastore.Txn) error { + collections, err := db.GetAllCollectionsTxn(ctx, txn) if err != nil { return err } diff --git a/db/sequence.go b/db/sequence.go index a2d71e09db..1fcfbf7872 100644 --- a/db/sequence.go +++ b/db/sequence.go @@ -17,29 +17,28 @@ import ( ds "github.com/ipfs/go-datastore" "github.com/sourcenetwork/defradb/core" + "github.com/sourcenetwork/defradb/datastore" "github.com/sourcenetwork/defradb/errors" ) type sequence struct { - db *db key core.SequenceKey val uint64 } -func (db *db) getSequence(ctx context.Context, key string) (*sequence, error) { +func (db *db) getSequence(ctx context.Context, txn datastore.Txn, key string) (*sequence, error) { if key == "" { return nil, ErrKeyEmpty } seqKey := core.NewSequenceKey(key) seq := &sequence{ - db: db, key: seqKey, val: uint64(0), } - _, err := seq.get(ctx) + _, err := seq.get(ctx, txn) if errors.Is(err, ds.ErrNotFound) { - err = seq.update(ctx) + err = seq.update(ctx, txn) if err != nil { return nil, err } @@ -50,8 +49,8 @@ func (db *db) getSequence(ctx context.Context, key string) (*sequence, error) { return seq, nil } -func (seq *sequence) get(ctx context.Context) (uint64, error) { - val, err := seq.db.systemstore().Get(ctx, seq.key.ToDS()) +func (seq *sequence) get(ctx context.Context, txn datastore.Txn) (uint64, error) { + val, err := txn.Systemstore().Get(ctx, seq.key.ToDS()) if err != nil { return 0, err } @@ -60,22 +59,22 @@ func (seq *sequence) get(ctx context.Context) (uint64, error) { return seq.val, nil } -func (seq *sequence) update(ctx context.Context) error { +func (seq *sequence) update(ctx context.Context, txn datastore.Txn) error { var buf [8]byte binary.BigEndian.PutUint64(buf[:], seq.val) - if err := seq.db.systemstore().Put(ctx, seq.key.ToDS(), buf[:]); err != nil { + if err := txn.Systemstore().Put(ctx, seq.key.ToDS(), buf[:]); err != nil { return err } return nil } -func (seq *sequence) next(ctx context.Context) (uint64, error) { - _, err := seq.get(ctx) +func (seq *sequence) next(ctx context.Context, txn datastore.Txn) (uint64, error) { + _, err := seq.get(ctx, txn) if err != nil { return 0, err } seq.val++ - return seq.val, seq.update(ctx) + return seq.val, seq.update(ctx, txn) } diff --git a/net/peer.go b/net/peer.go index daa15716fe..32eb629580 100644 --- a/net/peer.go +++ b/net/peer.go @@ -287,6 +287,27 @@ func (p *Peer) SetReplicator( ctx context.Context, paddr ma.Multiaddr, collectionNames ...string, +) (peer.ID, error) { + txn, err := p.db.NewTxn(ctx, true) + if err != nil { + return "", err + } + + pid, err := p.setReplicator(ctx, txn, paddr, collectionNames...) + if err != nil { + txn.Discard(ctx) + return "", err + } + + return pid, txn.Commit(ctx) +} + +// setReplicator adds a target peer node as a replication destination for documents in our DB. +func (p *Peer) setReplicator( + ctx context.Context, + txn datastore.Txn, + paddr ma.Multiaddr, + collectionNames ...string, ) (peer.ID, error) { var pid peer.ID @@ -295,7 +316,7 @@ func (p *Peer) SetReplicator( schemas := []string{} if len(collectionNames) == 0 { var err error - collections, err = p.db.GetAllCollections(ctx) + collections, err = p.db.GetAllCollectionsTxn(ctx, txn) if err != nil { return pid, errors.Wrap("failed to get all collections for replicator", err) } @@ -304,7 +325,7 @@ func (p *Peer) SetReplicator( } } else { for _, cName := range collectionNames { - col, err := p.db.GetCollectionByName(ctx, cName) + col, err := p.db.GetCollectionByNameTxn(ctx, txn, cName) if err != nil { return pid, errors.Wrap("failed to get collection for replicator", err) } @@ -466,6 +487,27 @@ func (p *Peer) DeleteReplicator( ctx context.Context, pid peer.ID, collectionNames ...string, +) error { + txn, err := p.db.NewTxn(ctx, true) + if err != nil { + return err + } + + err = p.deleteReplicator(ctx, txn, pid, collectionNames...) + if err != nil { + txn.Discard(ctx) + return err + } + + return txn.Commit(ctx) +} + +// DeleteReplicator adds a target peer node as a replication destination for documents in our DB. +func (p *Peer) deleteReplicator( + ctx context.Context, + txn datastore.Txn, + pid peer.ID, + collectionNames ...string, ) error { // make sure it's not ourselves if pid == p.host.ID() { @@ -477,7 +519,7 @@ func (p *Peer) DeleteReplicator( schemaMap := make(map[string]struct{}) if len(collectionNames) == 0 { var err error - collections, err := p.db.GetAllCollections(ctx) + collections, err := p.db.GetAllCollectionsTxn(ctx, txn) if err != nil { return errors.Wrap("failed to get all collections for replicator", err) } @@ -487,7 +529,7 @@ func (p *Peer) DeleteReplicator( } } else { for _, cName := range collectionNames { - col, err := p.db.GetCollectionByName(ctx, cName) + col, err := p.db.GetCollectionByNameTxn(ctx, txn, cName) if err != nil { return errors.Wrap("failed to get collection for replicator", err) } @@ -709,16 +751,22 @@ type EvtPubSub struct { // AddP2PCollectionTopic adds the collectionID to the pubsup topics func (p *Peer) AddP2PCollections(collections []string) error { + txn, err := p.db.NewTxn(p.ctx, false) + if err != nil { + return err + } + defer txn.Discard(p.ctx) + // first let's make sure the collections actually exists for _, col := range collections { - _, err := p.db.GetCollectionBySchemaID(p.ctx, col) + _, err := p.db.GetCollectionBySchemaIDTxn(p.ctx, txn, col) if err != nil { return err } } for _, col := range collections { - err := p.db.AddP2PCollection(p.ctx, col) + err := p.db.AddP2PCollectionTxn(p.ctx, txn, col) if err != nil { return err } @@ -728,13 +776,19 @@ func (p *Peer) AddP2PCollections(collections []string) error { } } - return nil + return txn.Commit(p.ctx) } // RemoveP2PCollectionTopics adds the collectionID from the pubsup topics func (p *Peer) RemoveP2PCollections(collections []string) error { + txn, err := p.db.NewTxn(p.ctx, false) + if err != nil { + return err + } + defer txn.Discard(p.ctx) + for _, col := range collections { - err := p.db.RemoveP2PCollection(p.ctx, col) + err := p.db.RemoveP2PCollectionTxn(p.ctx, txn, col) if err != nil { return err } @@ -743,20 +797,27 @@ func (p *Peer) RemoveP2PCollections(collections []string) error { return err } } - return nil + return txn.Commit(p.ctx) } // GetAllP2PCollections gets all the collectionIDs from the pubsup topics func (p *Peer) GetAllP2PCollections() ([]client.P2PCollection, error) { + txn, err := p.db.NewTxn(p.ctx, false) + if err != nil { + return nil, err + } + collections, err := p.db.GetAllP2PCollections(p.ctx) if err != nil { + txn.Discard(p.ctx) return nil, err } var p2pCols []client.P2PCollection for _, colID := range collections { - col, err := p.db.GetCollectionBySchemaID(p.ctx, colID) + col, err := p.db.GetCollectionBySchemaIDTxn(p.ctx, txn, colID) if err != nil { + txn.Discard(p.ctx) return nil, err } p2pCols = append(p2pCols, client.P2PCollection{ @@ -765,5 +826,5 @@ func (p *Peer) GetAllP2PCollections() ([]client.P2PCollection, error) { }) } - return p2pCols, nil + return p2pCols, txn.Commit(p.ctx) } diff --git a/net/server.go b/net/server.go index cfeb512dbf..f462c8420c 100644 --- a/net/server.go +++ b/net/server.go @@ -169,10 +169,6 @@ func (s *server) PushLog(ctx context.Context, req *pb.PushLogRequest) (*pb.PushL schemaID := string(req.Body.SchemaID) docKey := core.DataStoreKeyFromDocKey(req.Body.DocKey.DocKey) - col, err := s.db.GetCollectionBySchemaID(ctx, schemaID) - if err != nil { - return nil, errors.Wrap(fmt.Sprintf("Failed to get collection from schemaID %s", schemaID), err) - } var txnErr error for retry := 0; retry < s.peer.db.MaxTxnRetries(); retry++ { @@ -184,6 +180,11 @@ func (s *server) PushLog(ctx context.Context, req *pb.PushLogRequest) (*pb.PushL } defer txn.Discard(ctx) + col, err := s.db.GetCollectionBySchemaIDTxn(ctx, txn, schemaID) + if err != nil { + return nil, errors.Wrap(fmt.Sprintf("Failed to get collection from schemaID %s", schemaID), err) + } + // Create a new DAG service with the current transaction var getter format.NodeGetter = s.peer.newDAGSyncerTxn(txn) if sessionMaker, ok := getter.(SessionDAGSyncer); ok { diff --git a/planner/create.go b/planner/create.go index f5cf874748..02a1e2a671 100644 --- a/planner/create.go +++ b/planner/create.go @@ -150,7 +150,7 @@ func (p *Planner) CreateDoc(parsed *mapper.Mutation) (planNode, error) { } // get collection - col, err := p.db.GetCollectionByName(p.ctx, parsed.Name) + col, err := p.db.GetCollectionByNameTxn(p.ctx, p.txn, parsed.Name) if err != nil { return nil, err } diff --git a/planner/delete.go b/planner/delete.go index cb300ebdbf..71f3ec2c71 100644 --- a/planner/delete.go +++ b/planner/delete.go @@ -93,7 +93,7 @@ func (n *deleteNode) Explain() (map[string]any, error) { } func (p *Planner) DeleteDocs(parsed *mapper.Mutation) (planNode, error) { - col, err := p.db.GetCollectionByName(p.ctx, parsed.Name) + col, err := p.db.GetCollectionByNameTxn(p.ctx, p.txn, parsed.Name) if err != nil { return nil, err } diff --git a/planner/update.go b/planner/update.go index ac2eeb8e99..e25f24cdc1 100644 --- a/planner/update.go +++ b/planner/update.go @@ -133,7 +133,7 @@ func (p *Planner) UpdateDocs(parsed *mapper.Mutation) (planNode, error) { } // get collection - col, err := p.db.GetCollectionByName(p.ctx, parsed.Name) + col, err := p.db.GetCollectionByNameTxn(p.ctx, p.txn, parsed.Name) if err != nil { return nil, err } From e8978ceb06ab28850d600f87f6cc2e5c027a0b48 Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Thu, 16 Feb 2023 14:44:56 -0500 Subject: [PATCH 09/12] Make SetSchema (GQL) transactional Renames and changes AddSchema to SetSchema. SetSchema is now transactional, GQL type changes will now only be 'commited' on transaction commit, whilst allowing SetSchema to be called safely at any point during the lifetime of the transaction - allowing for the schema to be validated against GQL constraints before any changes have been persisted else where, and allowing those other changes to be executed/validated before any changes have been made to the GQL types. --- core/parser.go | 3 ++- db/schema.go | 27 ++++++++++++++++++++++----- request/graphql/parser.go | 23 +++++++++++++++++++---- tests/bench/query/planner/utils.go | 17 ++++++++++++++++- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/core/parser.go b/core/parser.go index 086c8e7afb..49f7040011 100644 --- a/core/parser.go +++ b/core/parser.go @@ -17,6 +17,7 @@ import ( "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/client/request" + "github.com/sourcenetwork/defradb/datastore" ) // SchemaDefinition represents a schema definition. @@ -49,5 +50,5 @@ type Parser interface { ParseSDL(ctx context.Context, schemaString string) ([]client.CollectionDescription, error) // Adds the given schema to this parser's model. - AddSchema(ctx context.Context, collections []client.CollectionDescription) error + SetSchema(ctx context.Context, txn datastore.Txn, collections []client.CollectionDescription) error } diff --git a/db/schema.go b/db/schema.go index aeec87c73e..99d531f066 100644 --- a/db/schema.go +++ b/db/schema.go @@ -26,17 +26,22 @@ func (db *db) AddSchema(ctx context.Context, schemaString string) error { } defer txn.Discard(ctx) - collectionDescriptions, err := db.parser.ParseSDL(ctx, schemaString) + existingDescriptions, err := db.getCollectionDescriptions(ctx, txn) if err != nil { return err } - err = db.parser.AddSchema(ctx, collectionDescriptions) + newDescriptions, err := db.parser.ParseSDL(ctx, schemaString) if err != nil { return err } - for _, desc := range collectionDescriptions { + err = db.parser.SetSchema(ctx, txn, append(existingDescriptions, newDescriptions...)) + if err != nil { + return err + } + + for _, desc := range newDescriptions { if _, err := db.CreateCollectionTxn(ctx, txn, desc); err != nil { return err } @@ -46,15 +51,27 @@ func (db *db) AddSchema(ctx context.Context, schemaString string) error { } func (db *db) loadSchema(ctx context.Context, txn datastore.Txn) error { - collections, err := db.GetAllCollectionsTxn(ctx, txn) + descriptions, err := db.getCollectionDescriptions(ctx, txn) if err != nil { return err } + return db.parser.SetSchema(ctx, txn, descriptions) +} + +func (db *db) getCollectionDescriptions( + ctx context.Context, + txn datastore.Txn, +) ([]client.CollectionDescription, error) { + collections, err := db.GetAllCollectionsTxn(ctx, txn) + if err != nil { + return nil, err + } + descriptions := make([]client.CollectionDescription, len(collections)) for i, collection := range collections { descriptions[i] = collection.Description() } - return db.parser.AddSchema(ctx, descriptions) + return descriptions, nil } diff --git a/request/graphql/parser.go b/request/graphql/parser.go index d02d5361a9..a562ef20fb 100644 --- a/request/graphql/parser.go +++ b/request/graphql/parser.go @@ -22,6 +22,7 @@ import ( "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" + "github.com/sourcenetwork/defradb/datastore" defrap "github.com/sourcenetwork/defradb/request/graphql/parser" "github.com/sourcenetwork/defradb/request/graphql/schema" ) @@ -29,7 +30,7 @@ import ( var _ core.Parser = (*parser)(nil) type parser struct { - schemaManager schema.SchemaManager + schemaManager *schema.SchemaManager } func NewParser() (*parser, error) { @@ -39,7 +40,7 @@ func NewParser() (*parser, error) { } p := &parser{ - schemaManager: *schemaManager, + schemaManager: schemaManager, } return p, nil @@ -102,8 +103,22 @@ func (p *parser) ParseSDL(ctx context.Context, schemaString string) ([]client.Co return schema.FromString(ctx, schemaString) } -func (p *parser) AddSchema(ctx context.Context, collections []client.CollectionDescription) error { - _, err := p.schemaManager.Generator.Generate(ctx, collections) +func (p *parser) SetSchema(ctx context.Context, txn datastore.Txn, collections []client.CollectionDescription) error { + schemaManager, err := schema.NewSchemaManager() + if err != nil { + return err + } + + _, err = schemaManager.Generator.Generate(ctx, collections) + if err != nil { + return err + } + + txn.OnSuccess( + func() { + p.schemaManager = schemaManager + }, + ) return err } diff --git a/tests/bench/query/planner/utils.go b/tests/bench/query/planner/utils.go index da1a79da78..659f387d0d 100644 --- a/tests/bench/query/planner/utils.go +++ b/tests/bench/query/planner/utils.go @@ -16,6 +16,7 @@ import ( "testing" "github.com/sourcenetwork/defradb/core" + "github.com/sourcenetwork/defradb/datastore" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/planner" "github.com/sourcenetwork/defradb/request/graphql" @@ -104,10 +105,24 @@ func buildParser( return nil, err } - err = parser.AddSchema(ctx, collectionDescriptions) + err = parser.SetSchema(ctx, &dummyTxn{}, collectionDescriptions) if err != nil { return nil, err } return parser, nil } + +var _ datastore.Txn = (*dummyTxn)(nil) + +type dummyTxn struct{} + +func (*dummyTxn) Rootstore() datastore.DSReaderWriter { return nil } +func (*dummyTxn) Datastore() datastore.DSReaderWriter { return nil } +func (*dummyTxn) Headstore() datastore.DSReaderWriter { return nil } +func (*dummyTxn) DAGstore() datastore.DAGStore { return nil } +func (*dummyTxn) Systemstore() datastore.DSReaderWriter { return nil } +func (*dummyTxn) Commit(ctx context.Context) error { return nil } +func (*dummyTxn) Discard(ctx context.Context) {} +func (*dummyTxn) OnSuccess(fn func()) {} +func (*dummyTxn) OnError(fn func()) {} From 12fb03e6d01425e2c943da2cfa081a497c5f96c8 Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Tue, 7 Feb 2023 16:54:00 -0500 Subject: [PATCH 10/12] Add support for adding fields to schema Please note that the client interfaces will be reworked in the near future so that the transaction related items clutter the primary interface less, and are more consistent. For now I have just followed the existing `Txn` suffix naming. --- client/db.go | 30 ++ db/collection.go | 177 ++++++++++- db/errors.go | 111 +++++++ db/schema.go | 86 +++++ go.mod | 1 + go.sum | 3 + .../updates/add/field/crdt/composite_test.go | 41 +++ .../updates/add/field/crdt/invalid_test.go | 41 +++ .../schema/updates/add/field/crdt/lww_test.go | 49 +++ .../updates/add/field/crdt/none_test.go | 81 +++++ .../add/field/crdt/object_bool_test.go | 41 +++ .../schema/updates/add/field/create_test.go | 122 +++++++ .../updates/add/field/create_update_test.go | 161 ++++++++++ .../updates/add/field/kind/bool_array_test.go | 93 ++++++ .../add/field/kind/bool_nil_array_test.go | 95 ++++++ .../updates/add/field/kind/bool_test.go | 93 ++++++ .../updates/add/field/kind/datetime_test.go | 93 ++++++ .../updates/add/field/kind/dockey_test.go | 93 ++++++ .../add/field/kind/float_array_test.go | 93 ++++++ .../add/field/kind/float_nil_array_test.go | 99 ++++++ .../updates/add/field/kind/float_test.go | 93 ++++++ .../field/kind/foreign_object_array_test.go | 41 +++ .../add/field/kind/foreign_object_test.go | 41 +++ .../updates/add/field/kind/int_array_test.go | 93 ++++++ .../add/field/kind/int_nil_array_test.go | 99 ++++++ .../schema/updates/add/field/kind/int_test.go | 93 ++++++ .../updates/add/field/kind/invalid_test.go | 189 +++++++++++ .../updates/add/field/kind/none_test.go | 41 +++ .../add/field/kind/string_array_test.go | 93 ++++++ .../add/field/kind/string_nil_array_test.go | 99 ++++++ .../updates/add/field/kind/string_test.go | 93 ++++++ .../schema/updates/add/field/simple_test.go | 297 ++++++++++++++++++ .../updates/add/field/with_filter_test.go | 92 ++++++ .../schema/updates/add/simple_test.go | 161 ++++++++++ .../schema/updates/copy/field/simple_test.go | 89 ++++++ .../schema/updates/copy/simple_test.go | 49 +++ .../schema/updates/move/field/simple_test.go | 42 +++ .../schema/updates/move/simple_test.go | 88 ++++++ .../updates/remove/fields/simple_test.go | 254 +++++++++++++++ .../schema/updates/remove/simple_test.go | 151 +++++++++ .../updates/replace/field/simple_test.go | 67 ++++ .../schema/updates/replace/simple_test.go | 111 +++++++ .../schema/updates/test/add_field_test.go | 84 +++++ .../schema/updates/test/field/simple_test.go | 112 +++++++ .../schema/updates/test/simple_test.go | 119 +++++++ tests/integration/test_case.go | 5 + tests/integration/utils2.go | 18 ++ 47 files changed, 4316 insertions(+), 1 deletion(-) create mode 100644 tests/integration/schema/updates/add/field/crdt/composite_test.go create mode 100644 tests/integration/schema/updates/add/field/crdt/invalid_test.go create mode 100644 tests/integration/schema/updates/add/field/crdt/lww_test.go create mode 100644 tests/integration/schema/updates/add/field/crdt/none_test.go create mode 100644 tests/integration/schema/updates/add/field/crdt/object_bool_test.go create mode 100644 tests/integration/schema/updates/add/field/create_test.go create mode 100644 tests/integration/schema/updates/add/field/create_update_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/bool_array_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/bool_nil_array_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/bool_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/datetime_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/dockey_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/float_array_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/float_nil_array_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/float_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/foreign_object_array_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/foreign_object_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/int_array_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/int_nil_array_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/int_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/invalid_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/none_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/string_array_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/string_nil_array_test.go create mode 100644 tests/integration/schema/updates/add/field/kind/string_test.go create mode 100644 tests/integration/schema/updates/add/field/simple_test.go create mode 100644 tests/integration/schema/updates/add/field/with_filter_test.go create mode 100644 tests/integration/schema/updates/add/simple_test.go create mode 100644 tests/integration/schema/updates/copy/field/simple_test.go create mode 100644 tests/integration/schema/updates/copy/simple_test.go create mode 100644 tests/integration/schema/updates/move/field/simple_test.go create mode 100644 tests/integration/schema/updates/move/simple_test.go create mode 100644 tests/integration/schema/updates/remove/fields/simple_test.go create mode 100644 tests/integration/schema/updates/remove/simple_test.go create mode 100644 tests/integration/schema/updates/replace/field/simple_test.go create mode 100644 tests/integration/schema/updates/replace/simple_test.go create mode 100644 tests/integration/schema/updates/test/add_field_test.go create mode 100644 tests/integration/schema/updates/test/field/simple_test.go create mode 100644 tests/integration/schema/updates/test/simple_test.go diff --git a/client/db.go b/client/db.go index ea32ec3f6f..7205a3c2ff 100644 --- a/client/db.go +++ b/client/db.go @@ -22,8 +22,38 @@ import ( type DB interface { AddSchema(context.Context, string) error + // PatchSchema takes the given JSON patch string and applies it to the set of CollectionDescriptions + // present in the database. + // + // It will also update the GQL types used by the query system. It will error and not apply any of the + // requested, valid updates should the net result of the patch result in an invalid state. The + // individual operations defined in the patch do not need to result in a valid state, only the net result + // of the full patch. + // + // 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. + PatchSchema(context.Context, string) error + CreateCollection(context.Context, CollectionDescription) (Collection, error) CreateCollectionTxn(context.Context, datastore.Txn, CollectionDescription) (Collection, error) + + // UpdateCollectionTxn updates the persisted collection description matching the name of the given + // description, to the values in the given description. + // + // It will validate the given description using [ValidateUpdateCollectionTxn] before updating it. + // + // The collection (including the schema version ID) will only be updated if any changes have actually + // been made, if the given description matches the current persisted description then no changes will be + // applied. + UpdateCollectionTxn(context.Context, datastore.Txn, CollectionDescription) (Collection, error) + + // ValidateUpdateCollectionTxn validates that the given collection description is a valid update. + // + // Will return true if the given desctiption differs from the current persisted state of the + // collection. Will return false and an error if it fails validation. + ValidateUpdateCollectionTxn(context.Context, datastore.Txn, CollectionDescription) (bool, error) + GetCollectionByName(context.Context, string) (Collection, error) GetCollectionByNameTxn(context.Context, datastore.Txn, string) (Collection, error) GetCollectionBySchemaID(context.Context, string) (Collection, error) diff --git a/db/collection.go b/db/collection.go index 124b3f2627..bd5d306961 100644 --- a/db/collection.go +++ b/db/collection.go @@ -25,6 +25,7 @@ import ( mh "github.com/multiformats/go-multihash" "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/datastore" "github.com/sourcenetwork/defradb/db/base" @@ -66,7 +67,7 @@ func (db *db) newCollection(desc client.CollectionDescription) (*collection, err } docKeyField := desc.Schema.Fields[0] - if docKeyField.Kind != client.FieldKind_DocKey || docKeyField.Name != "_key" { + if docKeyField.Kind != client.FieldKind_DocKey || docKeyField.Name != request.DocKeyFieldName { return nil, ErrSchemaFirstFieldDocKey } @@ -199,6 +200,180 @@ func (db *db) CreateCollectionTxn( return col, nil } +// UpdateCollectionTxn updates the persisted collection description matching the name of the given +// description, to the values in the given description. +// +// It will validate the given description using [ValidateUpdateCollectionTxn] before updating it. +// +// The collection (including the schema version ID) will only be updated if any changes have actually +// been made, if the given description matches the current persisted description then no changes will be +// applied. +func (db *db) UpdateCollectionTxn( + ctx context.Context, + txn datastore.Txn, + desc client.CollectionDescription, +) (client.Collection, error) { + hasChanged, err := db.ValidateUpdateCollectionTxn(ctx, txn, desc) + if err != nil { + return nil, err + } + + if !hasChanged { + return db.GetCollectionByNameTxn(ctx, txn, desc.Name) + } + + for i, field := range desc.Schema.Fields { + if field.ID == client.FieldID(0) { + // This is not wonderful and will probably break when we add the ability + // to delete fields, however it is good enough for now and matches the + // create behaviour. + field.ID = client.FieldID(i) + desc.Schema.Fields[i] = field + } + } + + globalSchemaBuf, err := json.Marshal(desc.Schema) + if err != nil { + return nil, err + } + + cid, err := core.NewSHA256CidV1(globalSchemaBuf) + if err != nil { + return nil, err + } + schemaVersionID := cid.String() + desc.Schema.VersionID = schemaVersionID + + buf, err := json.Marshal(desc) + if err != nil { + return nil, err + } + + collectionSchemaVersionKey := core.NewCollectionSchemaVersionKey(schemaVersionID) + // Whilst the schemaVersionKey is global, the data persisted at the key's location + // is local to the node (the global only elements are not useful beyond key generation). + err = txn.Systemstore().Put(ctx, collectionSchemaVersionKey.ToDS(), buf) + if err != nil { + return nil, err + } + + collectionSchemaKey := core.NewCollectionSchemaKey(desc.Schema.SchemaID) + err = txn.Systemstore().Put(ctx, collectionSchemaKey.ToDS(), []byte(schemaVersionID)) + if err != nil { + return nil, err + } + + collectionKey := core.NewCollectionKey(desc.Name) + err = txn.Systemstore().Put(ctx, collectionKey.ToDS(), []byte(schemaVersionID)) + if err != nil { + return nil, err + } + + return db.GetCollectionByNameTxn(ctx, txn, desc.Name) +} + +// ValidateUpdateCollectionTxn validates that the given collection description is a valid update. +// +// Will return true if the given desctiption differs from the current persisted state of the +// collection. Will return false and an error if it fails validation. +func (db *db) ValidateUpdateCollectionTxn( + ctx context.Context, + txn datastore.Txn, + proposedDesc client.CollectionDescription, +) (bool, error) { + var hasChanged bool + existingCollection, err := db.GetCollectionByNameTxn(ctx, txn, proposedDesc.Name) + if err != nil { + if err.Error() == "datastore: key not found" { + // Original error is quite unhelpful to users at the moment so we return a custom one + return false, NewErrAddCollectionWithPatch(proposedDesc.Name) + } + return false, err + } + existingDesc := existingCollection.Description() + + if proposedDesc.ID != existingDesc.ID { + return false, NewErrCollectionIDDoesntMatch(proposedDesc.Name, existingDesc.ID, proposedDesc.ID) + } + + if proposedDesc.Schema.SchemaID != existingDesc.Schema.SchemaID { + return false, NewErrSchemaIDDoesntMatch( + proposedDesc.Name, + existingDesc.Schema.SchemaID, + proposedDesc.Schema.SchemaID, + ) + } + + if proposedDesc.Schema.Name != existingDesc.Schema.Name { + // There is actually little reason to not support this atm besides controlling the surface area + // of the new feature. Changing this should not break anything, but it should be tested first. + return false, NewErrCannotModifySchemaName(existingDesc.Schema.Name, proposedDesc.Schema.Name) + } + + if proposedDesc.Schema.VersionID != "" && proposedDesc.Schema.VersionID != existingDesc.Schema.VersionID { + // If users specify this it will be overwritten, an error is prefered to quietly ignoring it. + return false, ErrCannotSetVersionID + } + + existingFieldsByID := map[client.FieldID]client.FieldDescription{} + existingFieldIndexesByName := map[string]int{} + for i, field := range existingDesc.Schema.Fields { + existingFieldIndexesByName[field.Name] = i + existingFieldsByID[field.ID] = field + } + + newFieldNames := map[string]struct{}{} + newFieldIds := map[client.FieldID]struct{}{} + for proposedIndex, proposedField := range proposedDesc.Schema.Fields { + var existingField client.FieldDescription + var fieldAlreadyExists bool + if proposedField.ID != client.FieldID(0) || + proposedField.Name == request.DocKeyFieldName { + existingField, fieldAlreadyExists = existingFieldsByID[proposedField.ID] + } + + if proposedField.ID != client.FieldID(0) && !fieldAlreadyExists { + return false, NewErrCannotSetFieldID(proposedField.Name, proposedField.ID) + } + + // If the field is new, then the collection has changed + hasChanged = hasChanged || !fieldAlreadyExists + + if !fieldAlreadyExists && (proposedField.Kind == client.FieldKind_FOREIGN_OBJECT || + proposedField.Kind == client.FieldKind_FOREIGN_OBJECT_ARRAY) { + return false, NewErrCannotAddRelationalField(proposedField.Name, proposedField.Kind) + } + + if _, isDuplicate := newFieldNames[proposedField.Name]; isDuplicate { + return false, NewErrDuplicateField(proposedField.Name) + } + + if fieldAlreadyExists && proposedField != existingField { + return false, NewErrCannotMutateField(proposedField.ID, proposedField.Name) + } + + if existingIndex := existingFieldIndexesByName[proposedField.Name]; fieldAlreadyExists && + proposedIndex != existingIndex { + return false, NewErrCannotMoveField(proposedField.Name, proposedIndex, existingIndex) + } + + if proposedField.Typ != client.NONE_CRDT && proposedField.Typ != client.LWW_REGISTER { + return false, NewErrInvalidCRDTType(proposedField.Name, proposedField.Typ) + } + + newFieldNames[proposedField.Name] = struct{}{} + newFieldIds[proposedField.ID] = struct{}{} + } + + for _, field := range existingDesc.Schema.Fields { + if _, stillExists := newFieldIds[field.ID]; !stillExists { + return false, NewErrCannotDeleteField(field.Name, field.ID) + } + } + + return hasChanged, nil +} + // getCollectionByVersionId returns the [*collection] at the given [schemaVersionId] version. // // Will return an error if the given key is empty, or not found. diff --git a/db/errors.go b/db/errors.go index 1307fd0b47..5aba8a4e00 100644 --- a/db/errors.go +++ b/db/errors.go @@ -11,6 +11,7 @@ package db import ( + "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/errors" ) @@ -21,6 +22,18 @@ const ( 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" ) var ( @@ -54,6 +67,18 @@ var ( 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) ) // NewErrFailedToGetHeads returns a new error indicating that the heads of a document @@ -93,3 +118,89 @@ func NewErrAddingP2PCollection(inner error) error { func NewErrRemovingP2PCollection(inner error) error { return errors.Wrap(errRemovingP2PCollection, inner) } + +func NewErrAddCollectionWithPatch(name string) error { + return errors.New( + errAddCollectionWithPatch, + errors.NewKV("Name", name), + ) +} + +func NewErrCollectionIDDoesntMatch(name string, existingID, proposedID uint32) error { + return errors.New( + errCollectionIDDoesntMatch, + errors.NewKV("Name", name), + errors.NewKV("ExistingID", existingID), + errors.NewKV("ProposedID", proposedID), + ) +} + +func NewErrSchemaIDDoesntMatch(name, existingID, proposedID string) error { + return errors.New( + errSchemaIDDoesntMatch, + errors.NewKV("Name", name), + errors.NewKV("ExistingID", existingID), + errors.NewKV("ProposedID", proposedID), + ) +} + +func NewErrCannotModifySchemaName(existingName, proposedName string) error { + return errors.New( + errCannotModifySchemaName, + errors.NewKV("ExistingName", existingName), + errors.NewKV("ProposedName", proposedName), + ) +} + +func NewErrCannotSetFieldID(name string, id client.FieldID) error { + return errors.New( + errCannotSetFieldID, + errors.NewKV("Field", name), + errors.NewKV("ID", id), + ) +} + +func NewErrCannotAddRelationalField(name string, kind client.FieldKind) error { + return errors.New( + errCannotAddRelationalField, + errors.NewKV("Field", name), + errors.NewKV("Kind", kind), + ) +} + +func NewErrDuplicateField(name string) error { + return errors.New(errDuplicateField, errors.NewKV("Name", name)) +} + +func NewErrCannotMutateField(id client.FieldID, name string) error { + return errors.New( + errCannotMutateField, + errors.NewKV("ID", id), + errors.NewKV("ProposedName", name), + ) +} + +func NewErrCannotMoveField(name string, proposedIndex, existingIndex int) error { + return errors.New( + errCannotMoveField, + errors.NewKV("Name", name), + errors.NewKV("ProposedIndex", proposedIndex), + errors.NewKV("ExistingIndex", existingIndex), + ) +} + +func NewErrInvalidCRDTType(name string, crdtType client.CType) error { + return errors.New( + errInvalidCRDTType, + errors.NewKV("Name", name), + errors.NewKV("CRDTType", crdtType), + ) +} + +func NewErrCannotDeleteField(name string, id client.FieldID) error { + return errors.New( + errCannotDeleteField, + errors.NewKV("Name", name), + errors.NewKV("ID", id), + ) +} diff --git a/db/schema.go b/db/schema.go index 99d531f066..54b8078dfa 100644 --- a/db/schema.go +++ b/db/schema.go @@ -12,6 +12,10 @@ package db import ( "context" + "encoding/json" + "strings" + + jsonpatch "github.com/evanphx/json-patch/v5" "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/datastore" @@ -75,3 +79,85 @@ func (db *db) getCollectionDescriptions( return descriptions, nil } + +// PatchSchema takes the given JSON patch string and applies it to the set of CollectionDescriptions +// present in the database. +// +// It will also update the GQL types used by the query system. It will error and not apply any of the +// requested, valid updates should the net result of the patch result in an invalid state. The +// individual operations defined in the patch do not need to result in a valid state, only the net result +// of the full patch. +// +// 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. +func (db *db) PatchSchema(ctx context.Context, patchString string) error { + txn, err := db.NewTxn(ctx, false) + if err != nil { + return err + } + defer txn.Discard(ctx) + + patch, err := jsonpatch.DecodePatch([]byte(patchString)) + if err != nil { + return err + } + + collectionsByName, err := db.getCollectionsByName(ctx, txn) + if err != nil { + return err + } + + existingDescriptionJson, err := json.Marshal(collectionsByName) + if err != nil { + return err + } + + newDescriptionJson, err := patch.Apply(existingDescriptionJson) + if err != nil { + return err + } + + var newDescriptionsByName map[string]client.CollectionDescription + decoder := json.NewDecoder(strings.NewReader(string(newDescriptionJson))) + decoder.DisallowUnknownFields() + err = decoder.Decode(&newDescriptionsByName) + if err != nil { + return err + } + + newDescriptions := []client.CollectionDescription{} + for _, desc := range newDescriptionsByName { + newDescriptions = append(newDescriptions, desc) + } + + for _, desc := range newDescriptions { + if _, err := db.UpdateCollectionTxn(ctx, txn, desc); err != nil { + return err + } + } + + err = db.parser.SetSchema(ctx, txn, newDescriptions) + if err != nil { + return err + } + + return txn.Commit(ctx) +} + +func (db *db) getCollectionsByName( + ctx context.Context, + txn datastore.Txn, +) (map[string]client.CollectionDescription, error) { + collections, err := db.GetAllCollectionsTxn(ctx, txn) + if err != nil { + return nil, err + } + + collectionsByName := map[string]client.CollectionDescription{} + for _, collection := range collections { + collectionsByName[collection.Name()] = collection.Description() + } + + return collectionsByName, nil +} diff --git a/go.mod b/go.mod index 7b7efd942f..697c4c9694 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/bxcodec/faker v2.0.1+incompatible github.com/dgraph-io/badger/v3 v3.2103.5 + github.com/evanphx/json-patch/v5 v5.6.0 github.com/fxamacker/cbor/v2 v2.4.0 github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/cors v1.2.1 diff --git a/go.sum b/go.sum index 0e88256edb..fe3b9fc02d 100644 --- a/go.sum +++ b/go.sum @@ -155,6 +155,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5/go.mod h1:JpoxHjuQauoxiFMl1ie8Xc/7TfLuMZ5eOCONd1sUBHg= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= @@ -490,6 +492,7 @@ github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0 github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= diff --git a/tests/integration/schema/updates/add/field/crdt/composite_test.go b/tests/integration/schema/updates/add/field/crdt/composite_test.go new file mode 100644 index 0000000000..54e5869899 --- /dev/null +++ b/tests/integration/schema/updates/add/field/crdt/composite_test.go @@ -0,0 +1,41 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package crdt + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldCRDTCompositeErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with crdt composite (3)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 2, "Typ":3} } + ] + `, + ExpectedError: "only default or LWW (last writer wins) CRDT types are supported. Name: Foo, CRDTType: 3", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/crdt/invalid_test.go b/tests/integration/schema/updates/add/field/crdt/invalid_test.go new file mode 100644 index 0000000000..78d1bccd4b --- /dev/null +++ b/tests/integration/schema/updates/add/field/crdt/invalid_test.go @@ -0,0 +1,41 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package crdt + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldCRDTInvalidErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with invalid CRDT (99)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 2, "Typ":99} } + ] + `, + ExpectedError: "only default or LWW (last writer wins) CRDT types are supported. Name: Foo, CRDTType: 99", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/crdt/lww_test.go b/tests/integration/schema/updates/add/field/crdt/lww_test.go new file mode 100644 index 0000000000..085b98c2f4 --- /dev/null +++ b/tests/integration/schema/updates/add/field/crdt/lww_test.go @@ -0,0 +1,49 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package crdt + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldCRDTLWW(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with crdt LWW (1)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 2, "Typ":1} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/crdt/none_test.go b/tests/integration/schema/updates/add/field/crdt/none_test.go new file mode 100644 index 0000000000..dd4d379330 --- /dev/null +++ b/tests/integration/schema/updates/add/field/crdt/none_test.go @@ -0,0 +1,81 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package crdt + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldCRDTDefault(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with crdt default", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 2} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldCRDTNone(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with crdt none (0)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 2, "Typ":0} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/crdt/object_bool_test.go b/tests/integration/schema/updates/add/field/crdt/object_bool_test.go new file mode 100644 index 0000000000..892b3b1102 --- /dev/null +++ b/tests/integration/schema/updates/add/field/crdt/object_bool_test.go @@ -0,0 +1,41 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package crdt + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldCRDTObjectWithBoolFieldErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field (bool) with crdt Object (2)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 2, "Typ":2} } + ] + `, + ExpectedError: "only default or LWW (last writer wins) CRDT types are supported. Name: Foo, CRDTType: 2", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/create_test.go b/tests/integration/schema/updates/add/field/create_test.go new file mode 100644 index 0000000000..8627a17ff1 --- /dev/null +++ b/tests/integration/schema/updates/add/field/create_test.go @@ -0,0 +1,122 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package field + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John" + }`, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + _key + Name + Email + } + }`, + Results: []map[string]any{ + { + "_key": "bae-43deba43-f2bc-59f4-9056-fef661b22832", + "Name": "John", + "Email": nil, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldWithCreateAfterSchemaUpdate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with create after schema update", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John" + }`, + }, + // We want to make sure that this works across database versions, so we tell + // the change detector to split here. + testUtils.SetupComplete{}, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "Shahzad", + "Email": "sqlizded@yahoo.ca" + }`, + }, + testUtils.Request{ + Request: `query { + Users { + _key + Name + Email + } + }`, + Results: []map[string]any{ + { + "_key": "bae-43deba43-f2bc-59f4-9056-fef661b22832", + "Name": "John", + "Email": nil, + }, + { + "_key": "bae-68926881-2eed-519b-b4eb-883b4a6624a6", + "Name": "Shahzad", + "Email": "sqlizded@yahoo.ca", + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/create_update_test.go b/tests/integration/schema/updates/add/field/create_update_test.go new file mode 100644 index 0000000000..318b1f2f08 --- /dev/null +++ b/tests/integration/schema/updates/add/field/create_update_test.go @@ -0,0 +1,161 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package field + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldWithCreateWithUpdateAfterSchemaUpdateAndVersionJoin(t *testing.T) { + initialSchemaVersionId := "bafkreicg3xcpjlt3ecguykpcjrdx5ogi4n7cq2fultyr6vippqdxnrny3u" + updatedSchemaVersionId := "bafkreicnj2kiq6vqxozxnhrc4mlbkdp5rr44awaetn5x5hcdymk6lxrxdy" + + test := testUtils.TestCase{ + Description: "Test schema update, add field with update after schema update, verison join", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John" + }`, + }, + // We want to make sure that this works across database versions, so we tell + // the change detector to split here. + testUtils.Request{ + Request: `query { + Users { + Name + _version { + schemaVersionId + } + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "_version": []map[string]any{ + { + "schemaVersionId": initialSchemaVersionId, + }, + }, + }, + }, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + }, + testUtils.UpdateDoc{ + CollectionID: 0, + DocID: 0, + Doc: `{ + "Email": "ih8oraclelicensing@netscape.net" + }`, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Email + _version { + schemaVersionId + } + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "Email": "ih8oraclelicensing@netscape.net", + "_version": []map[string]any{ + { + // Update commit + "schemaVersionId": updatedSchemaVersionId, + }, + { + // Create commit + "schemaVersionId": initialSchemaVersionId, + }, + }, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldWithCreateWithUpdateAfterSchemaUpdateAndCommitQuery(t *testing.T) { + initialSchemaVersionId := "bafkreicg3xcpjlt3ecguykpcjrdx5ogi4n7cq2fultyr6vippqdxnrny3u" + updatedSchemaVersionId := "bafkreicnj2kiq6vqxozxnhrc4mlbkdp5rr44awaetn5x5hcdymk6lxrxdy" + + test := testUtils.TestCase{ + Description: "Test schema update, add field with update after schema update, commits query", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John" + }`, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + }, + testUtils.UpdateDoc{ + CollectionID: 0, + DocID: 0, + Doc: `{ + "Email": "ih8oraclelicensing@netscape.net" + }`, + }, + testUtils.Request{ + Request: `query { + commits (field: "C") { + schemaVersionId + } + }`, + Results: []map[string]any{ + { + // Update commit + "schemaVersionId": updatedSchemaVersionId, + }, + { + // Create commit + "schemaVersionId": initialSchemaVersionId, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/bool_array_test.go b/tests/integration/schema/updates/add/field/kind/bool_array_test.go new file mode 100644 index 0000000000..4bad3433a4 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/bool_array_test.go @@ -0,0 +1,93 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindBoolArray(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind bool array (3)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 3} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindBoolArrayWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind bool array (3) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 3} } + ] + `, + }, + 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) +} diff --git a/tests/integration/schema/updates/add/field/kind/bool_nil_array_test.go b/tests/integration/schema/updates/add/field/kind/bool_nil_array_test.go new file mode 100644 index 0000000000..fd70aa7349 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/bool_nil_array_test.go @@ -0,0 +1,95 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindNillableBoolArray(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind nillable bool array (18)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 18} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindNillableBoolArrayWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind nillable bool array (18) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 18} } + ] + `, + }, + 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) +} diff --git a/tests/integration/schema/updates/add/field/kind/bool_test.go b/tests/integration/schema/updates/add/field/kind/bool_test.go new file mode 100644 index 0000000000..8649145ae3 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/bool_test.go @@ -0,0 +1,93 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindBool(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind bool (2)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 2} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindBoolWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind bool (2) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 2} } + ] + `, + }, + 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) +} diff --git a/tests/integration/schema/updates/add/field/kind/datetime_test.go b/tests/integration/schema/updates/add/field/kind/datetime_test.go new file mode 100644 index 0000000000..9a4cbc9479 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/datetime_test.go @@ -0,0 +1,93 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindDateTime(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind datetime (10)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 10} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindDateTimeWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind datetime (10) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 4} } + ] + `, + }, + 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) +} diff --git a/tests/integration/schema/updates/add/field/kind/dockey_test.go b/tests/integration/schema/updates/add/field/kind/dockey_test.go new file mode 100644 index 0000000000..ac4c3b06ab --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/dockey_test.go @@ -0,0 +1,93 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindDocKey(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind DocKey (1)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 1} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindDocKeyWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind DocKey (1) and create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 1} } + ] + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John", + "Foo": "nhgfdsfd" + }`, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "Foo": "nhgfdsfd", + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/float_array_test.go b/tests/integration/schema/updates/add/field/kind/float_array_test.go new file mode 100644 index 0000000000..d71164efe7 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/float_array_test.go @@ -0,0 +1,93 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindFloatArray(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind float array (7)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 7} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindFloatArrayWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind float array (7) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 7} } + ] + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John", + "Foo": [3.1, -8.1, 0] + }`, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "Foo": []float64{3.1, -8.1, 0}, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/float_nil_array_test.go b/tests/integration/schema/updates/add/field/kind/float_nil_array_test.go new file mode 100644 index 0000000000..de3388ea1f --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/float_nil_array_test.go @@ -0,0 +1,99 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindNillableFloatArray(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind nillable float array (20)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 20} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindNillableFloatArrayWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind nillable int array (20) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 20} } + ] + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John", + "Foo": [3.14, -5.77, null] + }`, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "Foo": []immutable.Option[float64]{ + immutable.Some(3.14), + immutable.Some(-5.77), + immutable.None[float64](), + }, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/float_test.go b/tests/integration/schema/updates/add/field/kind/float_test.go new file mode 100644 index 0000000000..ed231c2f11 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/float_test.go @@ -0,0 +1,93 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindFloat(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind float (6)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 6} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindFloatWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind float (6) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 6} } + ] + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John", + "Foo": 3 + }`, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "Foo": float64(3), + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} 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 new file mode 100644 index 0000000000..dc225320c9 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/foreign_object_array_test.go @@ -0,0 +1,41 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindForeignObjectArray(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object array (17)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "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", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} 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 new file mode 100644 index 0000000000..6e60249f67 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/foreign_object_test.go @@ -0,0 +1,41 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindForeignObject(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind foreign object (16)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "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", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/int_array_test.go b/tests/integration/schema/updates/add/field/kind/int_array_test.go new file mode 100644 index 0000000000..78b1db1d7f --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/int_array_test.go @@ -0,0 +1,93 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindIntArray(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind int array (5)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 5} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindIntArrayWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind int array (5) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 5} } + ] + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John", + "Foo": [3, 5, 8] + }`, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "Foo": []int64{3, 5, 8}, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/int_nil_array_test.go b/tests/integration/schema/updates/add/field/kind/int_nil_array_test.go new file mode 100644 index 0000000000..4e345b9055 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/int_nil_array_test.go @@ -0,0 +1,99 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindNillableIntArray(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind nillable int array (19)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 19} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindNillableIntArrayWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind nillable int array (19) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 19} } + ] + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John", + "Foo": [3, -5, null] + }`, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "Foo": []immutable.Option[int64]{ + immutable.Some[int64](3), + immutable.Some[int64](-5), + immutable.None[int64](), + }, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/int_test.go b/tests/integration/schema/updates/add/field/kind/int_test.go new file mode 100644 index 0000000000..e499139070 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/int_test.go @@ -0,0 +1,93 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindInt(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind int (4)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 4} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindIntWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind int (4) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 4} } + ] + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John", + "Foo": 3 + }`, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "Foo": uint64(3), + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/invalid_test.go b/tests/integration/schema/updates/add/field/kind/invalid_test.go new file mode 100644 index 0000000000..c7db512309 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/invalid_test.go @@ -0,0 +1,189 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKind8(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind deprecated (8)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 8} } + ] + `, + ExpectedError: "no type found for given name. Type: 8", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKind9(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind deprecated (9)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 9} } + ] + `, + ExpectedError: "no type found for given name. Type: 9", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKind13(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind deprecated (13)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 13} } + ] + `, + ExpectedError: "no type found for given name. Type: 13", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKind14(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind deprecated (14)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 14} } + ] + `, + ExpectedError: "no type found for given name. Type: 14", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKind15(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind deprecated (15)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 15} } + ] + `, + ExpectedError: "no type found for given name. Type: 15", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +// This test is currently the first unsupported value, if it becomes supported +// please update this test to be the newly lowest unsupported value. +func TestSchemaUpdatesAddFieldKind22(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind unsupported (22)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 22} } + ] + `, + ExpectedError: "no type found for given name. Type: 22", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +// Tests a semi-random but hardcoded unsupported kind to try and protect against anything odd permitting +// high values. +func TestSchemaUpdatesAddFieldKind198(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind unsupported (198)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 198} } + ] + `, + ExpectedError: "no type found for given name. Type: 198", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/none_test.go b/tests/integration/schema/updates/add/field/kind/none_test.go new file mode 100644 index 0000000000..9959fc2031 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/none_test.go @@ -0,0 +1,41 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindNone(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind none (0)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 0} } + ] + `, + ExpectedError: "no type found for given name. Type: 0", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/string_array_test.go b/tests/integration/schema/updates/add/field/kind/string_array_test.go new file mode 100644 index 0000000000..25300095f6 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/string_array_test.go @@ -0,0 +1,93 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindStringArray(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind string array (12)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 12} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindStringArrayWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind string array (12) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 12} } + ] + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John", + "Foo": ["bar", "pub", "inn", "out", "hokey", "cokey", "pepsi", "beer", "bar", "pub", "..."] + }`, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "Foo": []string{"bar", "pub", "inn", "out", "hokey", "cokey", "pepsi", "beer", "bar", "pub", "..."}, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/string_nil_array_test.go b/tests/integration/schema/updates/add/field/kind/string_nil_array_test.go new file mode 100644 index 0000000000..350adae4f2 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/string_nil_array_test.go @@ -0,0 +1,99 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindNillableStringArray(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind nillable string array (21)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 21} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindNillableStringArrayWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind nillable string array (21) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 21} } + ] + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John", + "Foo": ["hello", "پدر سگ", null] + }`, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "Foo": []immutable.Option[string]{ + immutable.Some("hello"), + immutable.Some("پدر سگ"), + immutable.None[string](), + }, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/kind/string_test.go b/tests/integration/schema/updates/add/field/kind/string_test.go new file mode 100644 index 0000000000..4bba3d64a5 --- /dev/null +++ b/tests/integration/schema/updates/add/field/kind/string_test.go @@ -0,0 +1,93 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package kind + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldKindString(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind string (11)", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 11} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldKindStringWithCreate(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field with kind string (11) with create", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Foo", "Kind": 11} } + ] + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John", + "Foo": "bar" + }`, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Foo + } + }`, + Results: []map[string]any{ + { + "Name": "John", + "Foo": "bar", + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/simple_test.go b/tests/integration/schema/updates/add/field/simple_test.go new file mode 100644 index 0000000000..9b24516644 --- /dev/null +++ b/tests/integration/schema/updates/add/field/simple_test.go @@ -0,0 +1,297 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package field + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldSimple(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Email + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldSimpleErrorsAddingToUnknownCollection(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add to unknown collection fails", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Authors/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + ExpectedError: "add operation does not apply: doc is missing path", + }, + testUtils.Request{ + Request: `query { + Users { + Name + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldMultipleInPatch(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add multiple fields in single patch", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} }, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "City", "Kind": 11} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Email + City + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldMultiplePatches(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add multiple patches", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "City", "Kind": 11} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Email + City + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldSimpleWithoutName(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field without name", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Kind": 11} } + ] + `, + ExpectedError: "Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \"\" does not.", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldMultipleInPatchPartialSuccess(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add multiple fields in single patch with rollback", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + // Email field is valid, City field has invalid kind + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} }, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "City", "Kind": 111} } + ] + `, + ExpectedError: "no type found for given name. Type: 111", + }, + testUtils.Request{ + // Email does not exist as the commit failed + Request: `query { + Users { + Name + Email + } + }`, + ExpectedError: "Cannot query field \"Email\" on type \"Users\"", + }, + testUtils.Request{ + // Original schema is preserved + Request: `query { + Users { + Name + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldSimpleDuplicateOfExistingField(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field that already exists", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Name", "Kind": 11} } + ] + `, + ExpectedError: "duplicate field. Name: Name", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldSimpleDuplicateField(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add duplicate fields", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} }, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + ExpectedError: "duplicate field. Name: Email", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldWithExplicitIDErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field that already exists", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"ID": 2, "Name": "Email", "Kind": 11} } + ] + `, + ExpectedError: "explicitly setting a field ID value is not supported. Field: Email, ID: 2", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/field/with_filter_test.go b/tests/integration/schema/updates/add/field/with_filter_test.go new file mode 100644 index 0000000000..4a8a7ce060 --- /dev/null +++ b/tests/integration/schema/updates/add/field/with_filter_test.go @@ -0,0 +1,92 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package field + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddFieldSimpleWithFilter(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field, query with new field as filter", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users (filter: {Name: {_eq: "John"}}) { + Name + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddFieldSimpleWithFilterOnPopulatedDatabase(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add field, query with new field as filter", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John" + }`, + }, + // We want to make sure that this works across database versions, so we tell + // the change detector to split here. + testUtils.SetupComplete{}, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users (filter: {Name: {_eq: "John"}}) { + Name + } + }`, + Results: []map[string]any{ + { + "Name": "John", + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/add/simple_test.go b/tests/integration/schema/updates/add/simple_test.go new file mode 100644 index 0000000000..91bb10d8bd --- /dev/null +++ b/tests/integration/schema/updates/add/simple_test.go @@ -0,0 +1,161 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package field + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesAddSimpleErrorsAddingSchema(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add schema fails", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/-", "value": {"Name": "Books"} } + ] + `, + ExpectedError: "unknown collection, adding collections via patch is not supported. Name: Books", + }, + testUtils.Request{ + Request: `query { + Users { + Name + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddSimpleErrorsAddingCollectionProp(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add collection property fails", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/-", "value": {"Name": "Books"} } + ] + `, + ExpectedError: `json: unknown field "-"`, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddSimpleErrorsAddingSchemaProp(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add schema property fails", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/-", "value": {"Foo": "Bar"} } + ] + `, + ExpectedError: `json: unknown field "-"`, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddSimpleErrorsAddingUnsupportedCollectionProp(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add to unsupported collection prop", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Foo/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + ExpectedError: "add operation does not apply: doc is missing path", + }, + testUtils.Request{ + Request: `query { + Users { + Name + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesAddSimpleErrorsAddingUnsupportedSchemaProp(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, add to unsupported schema prop", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Foo/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + ExpectedError: "add operation does not apply: doc is missing path", + }, + testUtils.Request{ + Request: `query { + Users { + Name + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/copy/field/simple_test.go b/tests/integration/schema/updates/copy/field/simple_test.go new file mode 100644 index 0000000000..c9b45e6aaa --- /dev/null +++ b/tests/integration/schema/updates/copy/field/simple_test.go @@ -0,0 +1,89 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package field + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesCopyFieldErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, copy field", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "copy", "from": "/Users/Schema/Fields/1", "path": "/Users/Schema/Fields/2" } + ] + `, + ExpectedError: "duplicate field. Name: Email", + }, + testUtils.Request{ + Request: `query { + Users { + Name + Email + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesCopyFieldWithRemoveIDAndReplaceName(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, copy field, rename and remove IDs", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + // Here we esentially use Email as a template, copying it, clearing the ID, and renaming the + // clone. + Patch: ` + [ + { "op": "copy", "from": "/Users/Schema/Fields/1", "path": "/Users/Schema/Fields/3" }, + { "op": "remove", "path": "/Users/Schema/Fields/3/ID" }, + { "op": "replace", "path": "/Users/Schema/Fields/3/Name", "value": "Fax" } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Email + Fax + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/copy/simple_test.go b/tests/integration/schema/updates/copy/simple_test.go new file mode 100644 index 0000000000..c79dd62c17 --- /dev/null +++ b/tests/integration/schema/updates/copy/simple_test.go @@ -0,0 +1,49 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package copy + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesCopyCollectionWithRemoveIDAndReplaceName(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, copy collection, rename and remove ids", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + // Here we esentially use Users as a template, copying it, clearing the IDs, and renaming the + // clone. It is deliberately blocked for now, but should function at somepoint. + Patch: ` + [ + { "op": "copy", "from": "/Users", "path": "/Book" }, + { "op": "remove", "path": "/Book/ID" }, + { "op": "remove", "path": "/Book/Schema/SchemaID" }, + { "op": "remove", "path": "/Book/Schema/VersionID" }, + { "op": "remove", "path": "/Book/Schema/Fields/1/ID" }, + { "op": "replace", "path": "/Book/Name", "value": "Book" }, + { "op": "replace", "path": "/Book/Schema/Name", "value": "Book" } + ] + `, + ExpectedError: "unknown collection, adding collections via patch is not supported. Name: Book", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/move/field/simple_test.go b/tests/integration/schema/updates/move/field/simple_test.go new file mode 100644 index 0000000000..8d3ca948ff --- /dev/null +++ b/tests/integration/schema/updates/move/field/simple_test.go @@ -0,0 +1,42 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package field + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesMoveFieldErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, move field", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "move", "from": "/Users/Schema/Fields/1", "path": "/Users/Schema/Fields/-" } + ] + `, + ExpectedError: "moving fields is not currently supported. Name: Name, ProposedIndex: 1, ExistingIndex: 2", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/move/simple_test.go b/tests/integration/schema/updates/move/simple_test.go new file mode 100644 index 0000000000..fcb5fcbca5 --- /dev/null +++ b/tests/integration/schema/updates/move/simple_test.go @@ -0,0 +1,88 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package move + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesMoveCollectionDoesNothing(t *testing.T) { + schemaVersionID := "bafkreicg3xcpjlt3ecguykpcjrdx5ogi4n7cq2fultyr6vippqdxnrny3u" + + test := testUtils.TestCase{ + Description: "Test schema update, move collection", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John" + }`, + }, + testUtils.SchemaPatch{ + // This just moves an object to a new key in a temporary dictionary, it doesn't actually do + // anything + Patch: ` + [ + { "op": "move", "from": "/Users", "path": "/Books" } + ] + `, + }, + testUtils.UpdateDoc{ + CollectionID: 0, + DocID: 0, + Doc: `{ + "Name": "Johnnn" + }`, + }, + testUtils.Request{ + // Assert that Users is still Users + Request: `query { + Users { + Name + } + }`, + Results: []map[string]any{ + { + "Name": "Johnnn", + }, + }, + }, + testUtils.Request{ + // Assert that the version ID remains the same + Request: `query { + commits (field: "C") { + schemaVersionId + } + }`, + Results: []map[string]any{ + { + // Update commit + "schemaVersionId": schemaVersionID, + }, + { + // Create commit + "schemaVersionId": schemaVersionID, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/remove/fields/simple_test.go b/tests/integration/schema/updates/remove/fields/simple_test.go new file mode 100644 index 0000000000..854a06d658 --- /dev/null +++ b/tests/integration/schema/updates/remove/fields/simple_test.go @@ -0,0 +1,254 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package fields + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesRemoveFieldErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove field", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Users/Schema/Fields/2" } + ] + `, + ExpectedError: "deleting an existing field is not supported. Name: Name", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesRemoveAllFieldsErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove all fields", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Users/Schema/Fields" } + ] + `, + ExpectedError: "deleting an existing field is not supported", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesRemoveFieldNameErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove field name", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Users/Schema/Fields/2/Name" } + ] + `, + ExpectedError: "mutating an existing field is not supported. ID: 2, ProposedName: ", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesRemoveFieldIDErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove field id", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Users/Schema/Fields/2/ID" } + ] + `, + ExpectedError: "deleting an existing field is not supported. Name: Name, ID: 2", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesRemoveFieldKindErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove field kind", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Users/Schema/Fields/2/Kind" } + ] + `, + ExpectedError: "mutating an existing field is not supported. ID: 2, ProposedName: ", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesRemoveFieldTypErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove field Typ", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Users/Schema/Fields/2/Typ" } + ] + `, + ExpectedError: "mutating an existing field is not supported. ID: 2, ProposedName: ", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesRemoveFieldSchemaErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove field Schema", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Author { + Name: String + Book: [Book] + } + type Book { + Name: String + Author: [Author] + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Author/Schema/Fields/1/Schema" } + ] + `, + ExpectedError: "mutating an existing field is not supported. ID: 1, ProposedName: Book", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Author", "Book"}, test) +} + +func TestSchemaUpdatesRemoveFieldRelationNameErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove field RelationName", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Author { + Name: String + Book: [Book] + } + type Book { + Name: String + Author: [Author] + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Author/Schema/Fields/1/RelationName" } + ] + `, + ExpectedError: "mutating an existing field is not supported. ID: 1, ProposedName: Book", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Author", "Book"}, test) +} + +func TestSchemaUpdatesRemoveFieldRelationTypeErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove field RelationType", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Author { + Name: String + Book: [Book] + } + type Book { + Name: String + Author: [Author] + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Author/Schema/Fields/1/RelationType" } + ] + `, + ExpectedError: "mutating an existing field is not supported. ID: 1, ProposedName: Book", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Author", "Book"}, test) +} diff --git a/tests/integration/schema/updates/remove/simple_test.go b/tests/integration/schema/updates/remove/simple_test.go new file mode 100644 index 0000000000..bedc42e95f --- /dev/null +++ b/tests/integration/schema/updates/remove/simple_test.go @@ -0,0 +1,151 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package remove + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesRemoveCollectionNameErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove collection name", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Users/Name" } + ] + `, + ExpectedError: "collection name can't be empty", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesRemoveCollectionIDErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove collection id", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Users/ID" } + ] + `, + ExpectedError: "CollectionID does not match existing. Name: Users, ExistingID: 1, ProposedID: 0", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesRemoveSchemaIDErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove schema ID", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Users/Schema/SchemaID" } + ] + `, + ExpectedError: "SchemaID does not match existing", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesRemoveSchemaVersionIDErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove schema version id", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + // This should do nothing + Patch: ` + [ + { "op": "remove", "path": "/Users/Schema/VersionID" } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Email + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesRemoveSchemaNameErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, remove schema name", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "remove", "path": "/Users/Schema/Name" } + ] + `, + ExpectedError: "modifying the schema name is not supported. ExistingName: Users, ProposedName: ", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/replace/field/simple_test.go b/tests/integration/schema/updates/replace/field/simple_test.go new file mode 100644 index 0000000000..35253de481 --- /dev/null +++ b/tests/integration/schema/updates/replace/field/simple_test.go @@ -0,0 +1,67 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package replace + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesReplaceFieldErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, replace field", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "replace", "path": "/Users/Schema/Fields/2", "value": {"Name": "Fax", "Kind": 11} } + ] + `, + ExpectedError: "deleting an existing field is not supported. Name: Name, ID: 2", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesReplaceFieldWithIDErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, replace field with correct ID", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + Email: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "replace", "path": "/Users/Schema/Fields/2", "value": {"ID":2, "Name": "Fax", "Kind": 11} } + ] + `, + ExpectedError: "mutating an existing field is not supported. ID: 2, ProposedName: Fax", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/replace/simple_test.go b/tests/integration/schema/updates/replace/simple_test.go new file mode 100644 index 0000000000..01b01cd990 --- /dev/null +++ b/tests/integration/schema/updates/replace/simple_test.go @@ -0,0 +1,111 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package replace + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesReplaceCollectionErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, replace collection", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + // Replace Users with Book + Patch: ` + [ + { + "op": "replace", "path": "/Users", "value": { + "Name": "Book", + "Schema": { + "Name": "Book", + "Fields": [ + {"Name": "Name", "Kind": 11} + ] + } + } + } + ] + `, + // WARNING: An error is still expected if/when we allow the adding of collections, as this also + // implies that the "Users" collection is to be deleted. Only once we support the adding *and* + // removal of collections should this not error. + ExpectedError: "unknown collection, adding collections via patch is not supported. Name: Book", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesReplaceCollectionNameWithExistingDoesNotChangeVersionID(t *testing.T) { + schemaVersionID := "bafkreicg3xcpjlt3ecguykpcjrdx5ogi4n7cq2fultyr6vippqdxnrny3u" + + test := testUtils.TestCase{ + Description: "Test schema update, replacing collection name with self does not change version ID", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John" + }`, + }, + testUtils.SchemaPatch{ + // This patch essentially does nothing, replacing the current value with the current value + Patch: ` + [ + { "op": "replace", "path": "/Users/Name", "value": "Users" } + ] + `, + }, + testUtils.UpdateDoc{ + CollectionID: 0, + DocID: 0, + Doc: `{ + "Name": "Johnnn" + }`, + }, + testUtils.Request{ + Request: `query { + commits (field: "C") { + schemaVersionId + } + }`, + Results: []map[string]any{ + { + // Update commit + "schemaVersionId": schemaVersionID, + }, + { + // Create commit + "schemaVersionId": schemaVersionID, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/test/add_field_test.go b/tests/integration/schema/updates/test/add_field_test.go new file mode 100644 index 0000000000..c205d9d1ea --- /dev/null +++ b/tests/integration/schema/updates/test/add_field_test.go @@ -0,0 +1,84 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesTestAddField(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, passing test allows new field", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "test", "path": "/Users/Schema/Name", "value": "Users" }, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + }, + testUtils.Request{ + Request: `query { + Users { + Name + Email + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesTestAddFieldBlockedByTest(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, failing test blocks new field", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "test", "path": "/Users/Schema/Name", "value": "Author" }, + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "Email", "Kind": 11} } + ] + `, + ExpectedError: "test failed", + }, + testUtils.Request{ + Request: `query { + Users { + Name + Email + } + }`, + ExpectedError: "Cannot query field \"Email\" on type \"Users\"", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/test/field/simple_test.go b/tests/integration/schema/updates/test/field/simple_test.go new file mode 100644 index 0000000000..1004fc27b9 --- /dev/null +++ b/tests/integration/schema/updates/test/field/simple_test.go @@ -0,0 +1,112 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesTestFieldNameErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, test field name passes", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "test", "path": "/Users/Schema/Fields/1/Name", "value": "Email" } + ] + `, + ExpectedError: "testing value /Users/Schema/Fields/1/Name failed: test failed", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesTestFieldNamePasses(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, test field name passes", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "test", "path": "/Users/Schema/Fields/1/Name", "value": "Name" } + ] + `, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesTestFieldErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, test field fails", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "test", "path": "/Users/Schema/Fields/1", "value": {"Name": "Name", "Kind": 11} } + ] + `, + ExpectedError: "testing value /Users/Schema/Fields/1 failed: test failed", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesTestFieldPasses(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, test field passes", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "test", "path": "/Users/Schema/Fields/1", "value": {"ID":1, "Name": "Name", "Kind": 11} } + ] + `, + ExpectedError: "testing value /Users/Schema/Fields/1 failed: test failed", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/schema/updates/test/simple_test.go b/tests/integration/schema/updates/test/simple_test.go new file mode 100644 index 0000000000..670e1e0e10 --- /dev/null +++ b/tests/integration/schema/updates/test/simple_test.go @@ -0,0 +1,119 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchemaUpdatesTestCollectionNameErrors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, test collection name", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "test", "path": "/Users/Name", "value": "Book" } + ] + `, + ExpectedError: "testing value /Users/Name failed: test failed", + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesTestCollectionNamePasses(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, test collection name passes", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "test", "path": "/Users/Name", "value": "Users" } + ] + `, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} + +func TestSchemaUpdatesTestCollectionNameDoesNotChangeVersionID(t *testing.T) { + schemaVersionID := "bafkreicg3xcpjlt3ecguykpcjrdx5ogi4n7cq2fultyr6vippqdxnrny3u" + + test := testUtils.TestCase{ + Description: "Test schema update, test collection name does not change version ID", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + Name: String + } + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "Name": "John" + }`, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "test", "path": "/Users/Name", "value": "Users" } + ] + `, + }, + testUtils.UpdateDoc{ + CollectionID: 0, + DocID: 0, + Doc: `{ + "Name": "Johnnn" + }`, + }, + testUtils.Request{ + Request: `query { + commits (field: "C") { + schemaVersionId + } + }`, + Results: []map[string]any{ + { + // Update commit + "schemaVersionId": schemaVersionID, + }, + { + // Create commit + "schemaVersionId": schemaVersionID, + }, + }, + }, + }, + } + testUtils.ExecuteTestCase(t, []string{"Users"}, test) +} diff --git a/tests/integration/test_case.go b/tests/integration/test_case.go index 74970b6ee4..e209f5b230 100644 --- a/tests/integration/test_case.go +++ b/tests/integration/test_case.go @@ -40,6 +40,11 @@ type SchemaUpdate struct { ExpectedError string } +type SchemaPatch struct { + Patch string + ExpectedError string +} + // CreateDoc will attempt to create the given document in the given collection // using the collection api. type CreateDoc struct { diff --git a/tests/integration/utils2.go b/tests/integration/utils2.go index 61842156ac..9396014031 100644 --- a/tests/integration/utils2.go +++ b/tests/integration/utils2.go @@ -327,6 +327,11 @@ func executeTestCase( // If the schema was updated we need to refresh the collection definitions. collections = getCollections(ctx, t, dbi.db, collectionNames) + case SchemaPatch: + patchSchema(ctx, t, dbi.db, testCase, action) + // If the schema was updated we need to refresh the collection definitions. + collections = getCollections(ctx, t, dbi.db, collectionNames) + case CreateDoc: documents = createDoc(ctx, t, testCase, collections, documents, action) @@ -478,6 +483,19 @@ func updateSchema( assertExpectedErrorRaised(t, testCase.Description, action.ExpectedError, expectedErrorRaised) } +func patchSchema( + ctx context.Context, + t *testing.T, + db client.DB, + testCase TestCase, + action SchemaPatch, +) { + err := db.PatchSchema(ctx, action.Patch) + expectedErrorRaised := AssertError(t, testCase.Description, err, action.ExpectedError) + + assertExpectedErrorRaised(t, testCase.Description, action.ExpectedError, expectedErrorRaised) +} + // createDoc creates a document using the collection api and caches it in the // given documents slice. func createDoc( From adf435be637daf6015f0417328f7768a0e9dff0a Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Mon, 6 Mar 2023 17:52:22 -0500 Subject: [PATCH 11/12] PR FIXUP - Reword ValidateUpdateCollectionTxn docs --- client/db.go | 2 +- db/collection.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/db.go b/client/db.go index 7205a3c2ff..1e1d8d78d5 100644 --- a/client/db.go +++ b/client/db.go @@ -51,7 +51,7 @@ type DB interface { // ValidateUpdateCollectionTxn validates that the given collection description is a valid update. // // Will return true if the given desctiption differs from the current persisted state of the - // collection. Will return false and an error if it fails validation. + // collection. Will return an error if it fails validation. ValidateUpdateCollectionTxn(context.Context, datastore.Txn, CollectionDescription) (bool, error) GetCollectionByName(context.Context, string) (Collection, error) diff --git a/db/collection.go b/db/collection.go index bd5d306961..7e617d8089 100644 --- a/db/collection.go +++ b/db/collection.go @@ -275,7 +275,7 @@ func (db *db) UpdateCollectionTxn( // ValidateUpdateCollectionTxn validates that the given collection description is a valid update. // // Will return true if the given desctiption differs from the current persisted state of the -// collection. Will return false and an error if it fails validation. +// collection. Will return an error if it fails validation. func (db *db) ValidateUpdateCollectionTxn( ctx context.Context, txn datastore.Txn, From 27093dd9ac6086f1ad5ee97d460a813946e06b10 Mon Sep 17 00:00:00 2001 From: Andrew Sisley Date: Mon, 6 Mar 2023 17:54:33 -0500 Subject: [PATCH 12/12] PR FIXUP - errors.Is and ds.ErrNotFound for err-if --- db/collection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/collection.go b/db/collection.go index 7e617d8089..416f321a35 100644 --- a/db/collection.go +++ b/db/collection.go @@ -284,7 +284,7 @@ func (db *db) ValidateUpdateCollectionTxn( var hasChanged bool existingCollection, err := db.GetCollectionByNameTxn(ctx, txn, proposedDesc.Name) if err != nil { - if err.Error() == "datastore: key not found" { + 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) }