diff --git a/client/db.go b/client/db.go index 02fee282bd..1e1d8d78d5 100644 --- a/client/db.go +++ b/client/db.go @@ -22,10 +22,44 @@ 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 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) - 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/descriptions.go b/client/descriptions.go index 4220a332d0..4b80476a00 100644 --- a/client/descriptions.go +++ b/client/descriptions.go @@ -136,18 +136,14 @@ 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 FieldKind_STRING_ARRAY FieldKind = 12 - FieldKind_BYTES FieldKind = 13 - - // Embedded object within the type - FieldKind_OBJECT FieldKind = 14 - - // Array of embedded objects - FieldKind_OBJECT_ARRAY FieldKind = 15 + _ FieldKind = 13 // safe to repurpose (was never used) + _ 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 @@ -185,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 @@ -201,7 +201,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/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/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/collection.go b/db/collection.go index 08323538fc..416f321a35 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 } @@ -91,15 +92,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 +128,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 +175,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 } @@ -179,16 +200,194 @@ func (db *db) CreateCollection( 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 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 errors.Is(err, ds.ErrNotFound) { + // Original error is quite unhelpful to users at the moment so we return a custom one + return false, NewErrAddCollectionWithPatch(proposedDesc.Name) + } + return false, err + } + 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. -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,48 +406,102 @@ 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.NewCollectionSchemaVersionKey("") - q, err := db.systemstore().Query(ctx, query.Query{ - Prefix: prefix.ToString(), - KeysOnly: true, + prefix := core.NewCollectionKey("") + q, err := txn.Systemstore().Query(ctx, query.Query{ + Prefix: prefix.ToString(), }) if err != nil { return nil, NewErrFailedToCreateCollectionQuery(err) @@ -265,8 +518,8 @@ func (db *db) GetAllCollections(ctx context.Context) ([]client.Collection, error return nil, err } - schemaVersionId := ds.NewKey(res.Key).BaseNamespace() - col, err := db.getCollectionByVersionId(ctx, schemaVersionId) + schemaVersionId := string(res.Value) + col, err := db.getCollectionByVersionId(ctx, txn, schemaVersionId) if err != nil { return nil, NewErrFailedToGetCollection(schemaVersionId, err) } diff --git a/db/collection_update.go b/db/collection_update.go index 2a0be023c3..2ac5cfb4d4 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: @@ -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 } 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/errors.go b/db/errors.go index 2becd9c527..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" ) @@ -20,7 +21,19 @@ 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" + 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/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..54b8078dfa 100644 --- a/db/schema.go +++ b/db/schema.go @@ -12,42 +12,152 @@ package db import ( "context" + "encoding/json" + "strings" + + jsonpatch "github.com/evanphx/json-patch/v5" "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 { - collectionDescriptions, err := db.parser.ParseSDL(ctx, schemaString) + txn, err := db.NewTxn(ctx, false) + if err != nil { + return err + } + defer txn.Discard(ctx) + + 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 { - if _, err := db.CreateCollection(ctx, desc); err != nil { + 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 } } - 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 { + 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 +} + +// 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/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/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/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 } 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/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/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()) {} 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{ 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(