From db1f41c2378273ec814abb043d4ecd3046a75eed Mon Sep 17 00:00:00 2001 From: AndrewSisley Date: Mon, 25 Sep 2023 10:49:47 -0400 Subject: [PATCH] feat: Allow setting of default schema version (#1888) ## Relevant issue(s) Resolves #1884 ## Description Allows setting of default schema version, allowing rapid switching between (application) api/database versions. It also allows them to define and apply schema updates eagerly, and then make the switch at a later date. --- api/http/handlerfuncs.go | 5 +- client/db.go | 14 +- client/mocks/db.go | 64 ++++- db/collection.go | 61 ++++- db/schema.go | 5 +- db/txn_db.go | 27 +- http/client.go | 25 +- http/handler_store.go | 22 +- http/server.go | 1 + http/wrapper.go | 8 +- .../migrations/query/with_set_default_test.go | 236 ++++++++++++++++++ .../schema/updates/add/field/simple_test.go | 35 +++ .../schema/with_update_set_default_test.go | 146 +++++++++++ tests/integration/test_case.go | 20 +- tests/integration/utils2.go | 27 +- 15 files changed, 656 insertions(+), 40 deletions(-) create mode 100644 tests/integration/schema/migrations/query/with_set_default_test.go create mode 100644 tests/integration/schema/with_update_set_default_test.go diff --git a/api/http/handlerfuncs.go b/api/http/handlerfuncs.go index e4163de05f..2a248d7d81 100644 --- a/api/http/handlerfuncs.go +++ b/api/http/handlerfuncs.go @@ -271,7 +271,10 @@ func patchSchemaHandler(rw http.ResponseWriter, req *http.Request) { return } - err = db.PatchSchema(req.Context(), string(patch)) + // Hardcode setDefault to true here, as that preserves the existing behaviour. + // This function will be ripped out very shortly and I don't think it is worth + // spending time/thought here. The new http api handles this correctly. + err = db.PatchSchema(req.Context(), string(patch), true) if err != nil { handleErr(req.Context(), rw, err, http.StatusInternalServerError) return diff --git a/client/db.go b/client/db.go index ba4dd0b89d..47cd7d5a85 100644 --- a/client/db.go +++ b/client/db.go @@ -96,7 +96,8 @@ type Store interface { AddSchema(context.Context, string) ([]CollectionDescription, error) // PatchSchema takes the given JSON patch string and applies it to the set of CollectionDescriptions - // present in the database. + // present in the database. If true is provided, the new schema versions will be made default, otherwise + // [SetDefaultSchemaVersion] should be called to set them so. // // 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 @@ -109,7 +110,16 @@ type Store interface { // // Field [FieldKind] values may be provided in either their raw integer form, or as string as per // [FieldKindStringToEnumMapping]. - PatchSchema(context.Context, string) error + PatchSchema(context.Context, string, bool) error + + // SetDefaultSchemaVersion sets the default schema version to the ID provided. It will be applied to all + // collections using the schema. + // + // This will affect all operations interacting with the schema where a schema version is not explicitly + // provided. This includes GQL queries and Collection operations. + // + // It will return an error if the provided schema version ID does not exist. + SetDefaultSchemaVersion(context.Context, string) error // SetMigration sets the migration for the given source-destination schema version IDs. Is equivilent to // calling `LensRegistry().SetMigration(ctx, cfg)`. diff --git a/client/mocks/db.go b/client/mocks/db.go index cb0af26193..02d32c4e8c 100644 --- a/client/mocks/db.go +++ b/client/mocks/db.go @@ -992,13 +992,13 @@ func (_c *DB_NewTxn_Call) RunAndReturn(run func(context.Context, bool) (datastor return _c } -// PatchSchema provides a mock function with given fields: _a0, _a1 -func (_m *DB) PatchSchema(_a0 context.Context, _a1 string) error { - ret := _m.Called(_a0, _a1) +// PatchSchema provides a mock function with given fields: _a0, _a1, _a2 +func (_m *DB) PatchSchema(_a0 context.Context, _a1 string, _a2 bool) error { + ret := _m.Called(_a0, _a1, _a2) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(_a0, _a1) + if rf, ok := ret.Get(0).(func(context.Context, string, bool) error); ok { + r0 = rf(_a0, _a1, _a2) } else { r0 = ret.Error(0) } @@ -1014,13 +1014,14 @@ type DB_PatchSchema_Call struct { // PatchSchema is a helper method to define mock.On call // - _a0 context.Context // - _a1 string -func (_e *DB_Expecter) PatchSchema(_a0 interface{}, _a1 interface{}) *DB_PatchSchema_Call { - return &DB_PatchSchema_Call{Call: _e.mock.On("PatchSchema", _a0, _a1)} +// - _a2 bool +func (_e *DB_Expecter) PatchSchema(_a0 interface{}, _a1 interface{}, _a2 interface{}) *DB_PatchSchema_Call { + return &DB_PatchSchema_Call{Call: _e.mock.On("PatchSchema", _a0, _a1, _a2)} } -func (_c *DB_PatchSchema_Call) Run(run func(_a0 context.Context, _a1 string)) *DB_PatchSchema_Call { +func (_c *DB_PatchSchema_Call) Run(run func(_a0 context.Context, _a1 string, _a2 bool)) *DB_PatchSchema_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) + run(args[0].(context.Context), args[1].(string), args[2].(bool)) }) return _c } @@ -1030,7 +1031,7 @@ func (_c *DB_PatchSchema_Call) Return(_a0 error) *DB_PatchSchema_Call { return _c } -func (_c *DB_PatchSchema_Call) RunAndReturn(run func(context.Context, string) error) *DB_PatchSchema_Call { +func (_c *DB_PatchSchema_Call) RunAndReturn(run func(context.Context, string, bool) error) *DB_PatchSchema_Call { _c.Call.Return(run) return _c } @@ -1163,6 +1164,49 @@ func (_c *DB_Root_Call) RunAndReturn(run func() datastore.RootStore) *DB_Root_Ca return _c } +// SetDefaultSchemaVersion provides a mock function with given fields: _a0, _a1 +func (_m *DB) SetDefaultSchemaVersion(_a0 context.Context, _a1 string) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DB_SetDefaultSchemaVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDefaultSchemaVersion' +type DB_SetDefaultSchemaVersion_Call struct { + *mock.Call +} + +// SetDefaultSchemaVersion is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 string +func (_e *DB_Expecter) SetDefaultSchemaVersion(_a0 interface{}, _a1 interface{}) *DB_SetDefaultSchemaVersion_Call { + return &DB_SetDefaultSchemaVersion_Call{Call: _e.mock.On("SetDefaultSchemaVersion", _a0, _a1)} +} + +func (_c *DB_SetDefaultSchemaVersion_Call) Run(run func(_a0 context.Context, _a1 string)) *DB_SetDefaultSchemaVersion_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *DB_SetDefaultSchemaVersion_Call) Return(_a0 error) *DB_SetDefaultSchemaVersion_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DB_SetDefaultSchemaVersion_Call) RunAndReturn(run func(context.Context, string) error) *DB_SetDefaultSchemaVersion_Call { + _c.Call.Return(run) + return _c +} + // SetMigration provides a mock function with given fields: _a0, _a1 func (_m *DB) SetMigration(_a0 context.Context, _a1 client.LensConfig) error { ret := _m.Called(_a0, _a1) diff --git a/db/collection.go b/db/collection.go index a9d3f5c403..cda5cbf584 100644 --- a/db/collection.go +++ b/db/collection.go @@ -234,6 +234,7 @@ func (db *db) updateCollection( existingDescriptionsByName map[string]client.CollectionDescription, proposedDescriptionsByName map[string]client.CollectionDescription, desc client.CollectionDescription, + setAsDefaultVersion bool, ) (client.Collection, error) { hasChanged, err := db.validateUpdateCollection(ctx, txn, existingDescriptionsByName, proposedDescriptionsByName, desc) if err != nil { @@ -300,24 +301,19 @@ func (db *db) updateCollection( 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 - } - schemaVersionHistoryKey := core.NewSchemaHistoryKey(desc.Schema.SchemaID, previousSchemaVersionID) err = txn.Systemstore().Put(ctx, schemaVersionHistoryKey.ToDS(), []byte(schemaVersionID)) if err != nil { return nil, err } + if setAsDefaultVersion { + err = db.setDefaultSchemaVersionExplicit(ctx, txn, desc.Name, desc.Schema.SchemaID, schemaVersionID) + if err != nil { + return nil, err + } + } + return db.getCollectionByName(ctx, txn, desc.Name) } @@ -591,6 +587,47 @@ func validateUpdateCollectionIndexes( return false, nil } +func (db *db) setDefaultSchemaVersion( + ctx context.Context, + txn datastore.Txn, + schemaVersionID string, +) error { + col, err := db.getCollectionByVersionID(ctx, txn, schemaVersionID) + if err != nil { + return err + } + + desc := col.Description() + err = db.setDefaultSchemaVersionExplicit(ctx, txn, desc.Name, desc.Schema.SchemaID, schemaVersionID) + if err != nil { + return err + } + + cols, err := db.getCollectionDescriptions(ctx, txn) + if err != nil { + return err + } + + return db.parser.SetSchema(ctx, txn, cols) +} + +func (db *db) setDefaultSchemaVersionExplicit( + ctx context.Context, + txn datastore.Txn, + collectionName string, + schemaID string, + schemaVersionID string, +) error { + collectionSchemaKey := core.NewCollectionSchemaKey(schemaID) + err := txn.Systemstore().Put(ctx, collectionSchemaKey.ToDS(), []byte(schemaVersionID)) + if err != nil { + return err + } + + collectionKey := core.NewCollectionKey(collectionName) + return txn.Systemstore().Put(ctx, collectionKey.ToDS(), []byte(schemaVersionID)) +} + // getCollectionByVersionId returns the [*collection] at the given [schemaVersionId] version. // // Will return an error if the given key is empty, or not found. diff --git a/db/schema.go b/db/schema.go index 5c5c0568f8..910f44f8c1 100644 --- a/db/schema.go +++ b/db/schema.go @@ -103,7 +103,7 @@ func (db *db) getCollectionDescriptions( // 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, txn datastore.Txn, patchString string) error { +func (db *db) patchSchema(ctx context.Context, txn datastore.Txn, patchString string, setAsDefaultVersion bool) error { patch, err := jsonpatch.DecodePatch([]byte(patchString)) if err != nil { return err @@ -144,10 +144,11 @@ func (db *db) patchSchema(ctx context.Context, txn datastore.Txn, patchString st } for i, desc := range newDescriptions { - col, err := db.updateCollection(ctx, txn, collectionsByName, newDescriptionsByName, desc) + col, err := db.updateCollection(ctx, txn, collectionsByName, newDescriptionsByName, desc, setAsDefaultVersion) if err != nil { return err } + newDescriptions[i] = col.Description() } diff --git a/db/txn_db.go b/db/txn_db.go index b307d96e35..b4cc32dee1 100644 --- a/db/txn_db.go +++ b/db/txn_db.go @@ -250,14 +250,14 @@ func (db *explicitTxnDB) AddSchema(ctx context.Context, schemaString string) ([] // 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 *implicitTxnDB) PatchSchema(ctx context.Context, patchString string) error { +func (db *implicitTxnDB) PatchSchema(ctx context.Context, patchString string, setAsDefaultVersion bool) error { txn, err := db.NewTxn(ctx, false) if err != nil { return err } defer txn.Discard(ctx) - err = db.patchSchema(ctx, txn, patchString) + err = db.patchSchema(ctx, txn, patchString, setAsDefaultVersion) if err != nil { return err } @@ -276,8 +276,27 @@ func (db *implicitTxnDB) PatchSchema(ctx context.Context, patchString string) er // 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 *explicitTxnDB) PatchSchema(ctx context.Context, patchString string) error { - return db.patchSchema(ctx, db.txn, patchString) +func (db *explicitTxnDB) PatchSchema(ctx context.Context, patchString string, setAsDefaultVersion bool) error { + return db.patchSchema(ctx, db.txn, patchString, setAsDefaultVersion) +} + +func (db *implicitTxnDB) SetDefaultSchemaVersion(ctx context.Context, schemaVersionID string) error { + txn, err := db.NewTxn(ctx, false) + if err != nil { + return err + } + defer txn.Discard(ctx) + + err = db.setDefaultSchemaVersion(ctx, txn, schemaVersionID) + if err != nil { + return err + } + + return txn.Commit(ctx) +} + +func (db *explicitTxnDB) SetDefaultSchemaVersion(ctx context.Context, schemaVersionID string) error { + return db.setDefaultSchemaVersion(ctx, db.txn, schemaVersionID) } func (db *implicitTxnDB) SetMigration(ctx context.Context, cfg client.LensConfig) error { diff --git a/http/client.go b/http/client.go index 16a8924a65..867cdc3bb1 100644 --- a/http/client.go +++ b/http/client.go @@ -212,10 +212,31 @@ func (c *Client) AddSchema(ctx context.Context, schema string) ([]client.Collect return cols, nil } -func (c *Client) PatchSchema(ctx context.Context, patch string) error { +type patchSchemaRequest struct { + Patch string + SetAsDefaultVersion bool +} + +func (c *Client) PatchSchema(ctx context.Context, patch string, setAsDefaultVersion bool) error { methodURL := c.http.baseURL.JoinPath("schema") - req, err := http.NewRequestWithContext(ctx, http.MethodPatch, methodURL.String(), strings.NewReader(patch)) + body, err := json.Marshal(patchSchemaRequest{patch, setAsDefaultVersion}) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, methodURL.String(), bytes.NewBuffer(body)) + if err != nil { + return err + } + _, err = c.http.request(req) + return err +} + +func (c *Client) SetDefaultSchemaVersion(ctx context.Context, schemaVersionID string) error { + methodURL := c.http.baseURL.JoinPath("schema", "default") + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, methodURL.String(), strings.NewReader(schemaVersionID)) if err != nil { return err } diff --git a/http/handler_store.go b/http/handler_store.go index d0cbdf42d2..945f6115f8 100644 --- a/http/handler_store.go +++ b/http/handler_store.go @@ -151,12 +151,30 @@ func (s *storeHandler) AddSchema(rw http.ResponseWriter, req *http.Request) { func (s *storeHandler) PatchSchema(rw http.ResponseWriter, req *http.Request) { store := req.Context().Value(storeContextKey).(client.Store) - patch, err := io.ReadAll(req.Body) + var message patchSchemaRequest + err := requestJSON(req, &message) if err != nil { responseJSON(rw, http.StatusBadRequest, errorResponse{err}) return } - err = store.PatchSchema(req.Context(), string(patch)) + + err = store.PatchSchema(req.Context(), message.Patch, message.SetAsDefaultVersion) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + rw.WriteHeader(http.StatusOK) +} + +func (s *storeHandler) SetDefaultSchemaVersion(rw http.ResponseWriter, req *http.Request) { + store := req.Context().Value(storeContextKey).(client.Store) + + schemaVersionID, err := io.ReadAll(req.Body) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + err = store.SetDefaultSchemaVersion(req.Context(), string(schemaVersionID)) if err != nil { responseJSON(rw, http.StatusBadRequest, errorResponse{err}) return diff --git a/http/server.go b/http/server.go index 92da350aa1..7ad21e0632 100644 --- a/http/server.go +++ b/http/server.go @@ -54,6 +54,7 @@ func NewServer(db client.DB) *Server { api.Route("/schema", func(schema chi.Router) { schema.Post("/", store_handler.AddSchema) schema.Patch("/", store_handler.PatchSchema) + schema.Post("/default", store_handler.SetDefaultSchemaVersion) }) api.Route("/collections", func(collections chi.Router) { collections.Get("/", store_handler.GetCollection) diff --git a/http/wrapper.go b/http/wrapper.go index 558dc79474..eb91ffdb7a 100644 --- a/http/wrapper.go +++ b/http/wrapper.go @@ -86,8 +86,12 @@ func (w *Wrapper) AddSchema(ctx context.Context, schema string) ([]client.Collec return w.client.AddSchema(ctx, schema) } -func (w *Wrapper) PatchSchema(ctx context.Context, patch string) error { - return w.client.PatchSchema(ctx, patch) +func (w *Wrapper) PatchSchema(ctx context.Context, patch string, setAsDefaultVersion bool) error { + return w.client.PatchSchema(ctx, patch, setAsDefaultVersion) +} + +func (w *Wrapper) SetDefaultSchemaVersion(ctx context.Context, schemaVersionID string) error { + return w.client.SetDefaultSchemaVersion(ctx, schemaVersionID) } func (w *Wrapper) SetMigration(ctx context.Context, config client.LensConfig) error { diff --git a/tests/integration/schema/migrations/query/with_set_default_test.go b/tests/integration/schema/migrations/query/with_set_default_test.go new file mode 100644 index 0000000000..e276bcab24 --- /dev/null +++ b/tests/integration/schema/migrations/query/with_set_default_test.go @@ -0,0 +1,236 @@ +// 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 query + +import ( + "testing" + + "github.com/lens-vm/lens/host-go/config/model" + "github.com/sourcenetwork/immutable" + + "github.com/sourcenetwork/defradb/client" + testUtils "github.com/sourcenetwork/defradb/tests/integration" + "github.com/sourcenetwork/defradb/tests/lenses" +) + +func TestSchemaMigrationQuery_WithSetDefaultToLatest_AppliesForwardMigration(t *testing.T) { + schemaVersionID2 := "bafkreigfqdqnj5dunwgcsf2a6ht6q6m2yv3ys6byw5ifsmi5lfcpeh5t7e" + + test := testUtils.TestCase{ + Description: "Test schema migration", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + verified: Boolean + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "John" + }`, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "email", "Kind": "String"} } + ] + `, + SetAsDefaultVersion: immutable.Some(false), + }, + testUtils.ConfigureMigration{ + LensConfig: client.LensConfig{ + SourceSchemaVersionID: "bafkreifmgqtwpvepenteuvj27u4ewix6nb7ypvyz6j555wsk5u2n7hrldm", + DestinationSchemaVersionID: schemaVersionID2, + Lens: model.Lens{ + Lenses: []model.LensModule{ + { + Path: lenses.SetDefaultModulePath, + Arguments: map[string]any{ + "dst": "verified", + "value": true, + }, + }, + }, + }, + }, + }, + testUtils.SetDefaultSchemaVersion{ + SchemaVersionID: schemaVersionID2, + }, + testUtils.Request{ + Request: `query { + Users { + name + verified + } + }`, + Results: []map[string]any{ + { + "name": "John", + "verified": true, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaMigrationQuery_WithSetDefaultToOriginal_AppliesInverseMigration(t *testing.T) { + schemaVersionID1 := "bafkreifmgqtwpvepenteuvj27u4ewix6nb7ypvyz6j555wsk5u2n7hrldm" + schemaVersionID2 := "bafkreigfqdqnj5dunwgcsf2a6ht6q6m2yv3ys6byw5ifsmi5lfcpeh5t7e" + + test := testUtils.TestCase{ + Description: "Test schema migration", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + verified: Boolean + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "email", "Kind": "String"} } + ] + `, + SetAsDefaultVersion: immutable.Some(false), + }, + testUtils.SetDefaultSchemaVersion{ + SchemaVersionID: schemaVersionID2, + }, + // Create John using the new schema version + testUtils.CreateDoc{ + Doc: `{ + "name": "John", + "verified": true + }`, + }, + testUtils.ConfigureMigration{ + LensConfig: client.LensConfig{ + SourceSchemaVersionID: schemaVersionID1, + DestinationSchemaVersionID: schemaVersionID2, + Lens: model.Lens{ + Lenses: []model.LensModule{ + { + Path: lenses.SetDefaultModulePath, + Arguments: map[string]any{ + "dst": "verified", + "value": true, + }, + }, + }, + }, + }, + }, + // Set the schema version back to the original + testUtils.SetDefaultSchemaVersion{ + SchemaVersionID: schemaVersionID1, + }, + testUtils.Request{ + Request: `query { + Users { + name + verified + } + }`, + Results: []map[string]any{ + { + "name": "John", + // The inverse lens migration has been applied, clearing the verified field + "verified": nil, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestSchemaMigrationQuery_WithSetDefaultToOriginalVersionThatDocWasCreatedAt_ClearsMigrations(t *testing.T) { + schemaVersionID1 := "bafkreifmgqtwpvepenteuvj27u4ewix6nb7ypvyz6j555wsk5u2n7hrldm" + schemaVersionID2 := "bafkreigfqdqnj5dunwgcsf2a6ht6q6m2yv3ys6byw5ifsmi5lfcpeh5t7e" + + test := testUtils.TestCase{ + Description: "Test schema migration", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + verified: Boolean + } + `, + }, + // Create John using the original schema version + testUtils.CreateDoc{ + Doc: `{ + "name": "John", + "verified": false + }`, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "email", "Kind": "String"} } + ] + `, + SetAsDefaultVersion: immutable.Some(true), + }, + testUtils.ConfigureMigration{ + LensConfig: client.LensConfig{ + SourceSchemaVersionID: schemaVersionID1, + DestinationSchemaVersionID: schemaVersionID2, + Lens: model.Lens{ + Lenses: []model.LensModule{ + { + Path: lenses.SetDefaultModulePath, + Arguments: map[string]any{ + "dst": "verified", + "value": true, + }, + }, + }, + }, + }, + }, + // Set the schema version back to the original + testUtils.SetDefaultSchemaVersion{ + SchemaVersionID: schemaVersionID1, + }, + testUtils.Request{ + Request: `query { + Users { + name + verified + } + }`, + Results: []map[string]any{ + { + "name": "John", + // The inverse lens migration has not been applied, the document is returned as it was defined + "verified": false, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/schema/updates/add/field/simple_test.go b/tests/integration/schema/updates/add/field/simple_test.go index d64f9e3bbe..1fe6980a62 100644 --- a/tests/integration/schema/updates/add/field/simple_test.go +++ b/tests/integration/schema/updates/add/field/simple_test.go @@ -13,6 +13,8 @@ package field import ( "testing" + "github.com/sourcenetwork/immutable" + testUtils "github.com/sourcenetwork/defradb/tests/integration" ) @@ -48,6 +50,39 @@ func TestSchemaUpdatesAddFieldSimple(t *testing.T) { testUtils.ExecuteTestCase(t, test) } +func TestSchemaUpdates_AddFieldSimpleDoNotSetDefault_Errors(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} } + ] + `, + SetAsDefaultVersion: immutable.Some(false), + }, + testUtils.Request{ + Request: `query { + Users { + name + email + } + }`, + ExpectedError: `Cannot query field "email" on type "Users".`, + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + func TestSchemaUpdatesAddFieldSimpleErrorsAddingToUnknownCollection(t *testing.T) { test := testUtils.TestCase{ Description: "Test schema update, add to unknown collection fails", diff --git a/tests/integration/schema/with_update_set_default_test.go b/tests/integration/schema/with_update_set_default_test.go new file mode 100644 index 0000000000..3b365e0e5f --- /dev/null +++ b/tests/integration/schema/with_update_set_default_test.go @@ -0,0 +1,146 @@ +// 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 schema + +import ( + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestSchema_WithUpdateAndSetDefaultVersionToEmptyString_Errors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, set default version to empty string", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "email", "Kind": 11} } + ] + `, + }, + testUtils.SetDefaultSchemaVersion{ + SchemaVersionID: "", + ExpectedError: "schema version ID can't be empty", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchema_WithUpdateAndSetDefaultVersionToUnknownVersion_Errors(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, set default version to invalid string", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "email", "Kind": 11} } + ] + `, + }, + testUtils.SetDefaultSchemaVersion{ + SchemaVersionID: "does not exist", + ExpectedError: "datastore: key not found", + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchema_WithUpdateAndSetDefaultVersionToOriginal_NewFieldIsNotQueriable(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, set default version to original schema version", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "email", "Kind": 11} } + ] + `, + SetAsDefaultVersion: immutable.Some(false), + }, + testUtils.SetDefaultSchemaVersion{ + SchemaVersionID: "bafkreihn4qameldz3j7rfundmd4ldhxnaircuulk6h2vcwnpcgxl4oqffq", + }, + testUtils.Request{ + Request: `query { + Users { + name + email + } + }`, + // As the email field did not exist at this schema version, it will return a gql error + ExpectedError: `Cannot query field "email" on type "Users".`, + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} + +func TestSchema_WithUpdateAndSetDefaultVersionToNew_AllowsQueryingOfNewField(t *testing.T) { + test := testUtils.TestCase{ + Description: "Test schema update, set default version to new schema version", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Users/Schema/Fields/-", "value": {"Name": "email", "Kind": 11} } + ] + `, + SetAsDefaultVersion: immutable.Some(false), + }, + testUtils.SetDefaultSchemaVersion{ + SchemaVersionID: "bafkreidejaxpsevyijnr4nah4e2l263emwhdaj57fwwv34eu5rea4ff54e", + }, + testUtils.Request{ + Request: `query { + Users { + name + email + } + }`, + Results: []map[string]any{}, + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/test_case.go b/tests/integration/test_case.go index e17adfdeaa..10f3cf7262 100644 --- a/tests/integration/test_case.go +++ b/tests/integration/test_case.go @@ -81,8 +81,24 @@ type SchemaPatch struct { // If a value is not provided the patch will be applied to all nodes. NodeID immutable.Option[int] - Patch string - ExpectedError string + Patch string + + // If SetAsDefaultVersion has a value, and that value is false then the schema version + // resulting from this patch will not be made default. + SetAsDefaultVersion immutable.Option[bool] + ExpectedError string +} + +// SetDefaultSchemaVersion is an action that will set the default schema version to the +// given value. +type SetDefaultSchemaVersion struct { + // NodeID may hold the ID (index) of a node to set the default schema version on. + // + // If a value is not provided the default will be set on all nodes. + NodeID immutable.Option[int] + + SchemaVersionID string + ExpectedError string } // CreateDoc will attempt to create the given document in the given collection diff --git a/tests/integration/utils2.go b/tests/integration/utils2.go index f722516445..f41e1a7485 100644 --- a/tests/integration/utils2.go +++ b/tests/integration/utils2.go @@ -384,6 +384,9 @@ func executeTestCase( case SchemaPatch: patchSchema(s, action) + case SetDefaultSchemaVersion: + setDefaultSchemaVersion(s, action) + case ConfigureMigration: configureMigration(s, action) @@ -1030,7 +1033,14 @@ func patchSchema( action SchemaPatch, ) { for _, node := range getNodes(action.NodeID, s.nodes) { - err := node.DB.PatchSchema(s.ctx, action.Patch) + var setAsDefaultVersion bool + if action.SetAsDefaultVersion.HasValue() { + setAsDefaultVersion = action.SetAsDefaultVersion.Value() + } else { + setAsDefaultVersion = true + } + + err := node.DB.PatchSchema(s.ctx, action.Patch, setAsDefaultVersion) expectedErrorRaised := AssertError(s.t, s.testCase.Description, err, action.ExpectedError) assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) @@ -1041,6 +1051,21 @@ func patchSchema( refreshIndexes(s) } +func setDefaultSchemaVersion( + s *state, + action SetDefaultSchemaVersion, +) { + for _, node := range getNodes(action.NodeID, s.nodes) { + err := node.DB.SetDefaultSchemaVersion(s.ctx, action.SchemaVersionID) + expectedErrorRaised := AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + + assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) + } + + refreshCollections(s) + refreshIndexes(s) +} + // createDoc creates a document using the chosen [mutationType] and caches it in the // test state object. func createDoc(