From 0eeae9de809d8fb6efae60490ff34208358cb6a2 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Mon, 12 Dec 2022 19:19:31 +0200 Subject: [PATCH] updated random_test --- daos/record.go | 287 +++++++++++---------------------- daos/record_table_sync.go | 147 +++++++++++++++++ daos/record_table_sync_test.go | 119 ++++++++++++++ daos/record_test.go | 190 ++++++++++++---------- tools/security/random_test.go | 2 +- 5 files changed, 461 insertions(+), 284 deletions(-) create mode 100644 daos/record_table_sync.go create mode 100644 daos/record_table_sync_test.go diff --git a/daos/record.go b/daos/record.go index 21f237582..91104d6ba 100644 --- a/daos/record.go +++ b/daos/record.go @@ -362,96 +362,128 @@ func (dao *Dao) DeleteRecord(record *models.Record) error { // run all consequent DeleteRecord requests synchroniously // to minimize SQLITE_BUSY errors if len(refs) > 0 { - ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() if err := dao.Block(ctx); err != nil { - return err + // ignore blocking and try to run directly... + } else { + defer dao.Continue() } - defer dao.Continue() } return dao.RunInTransaction(func(txDao *Dao) error { - // always delete the record first to ensure that there will be no "A<->B" - // relations to prevent deadlock when calling DeleteRecord recursively + // manually trigger delete on any linked external auth to ensure + // that the `OnModel*` hooks are triggered. + // + // note: the select is outside of the transaction to minimize + // SQLITE_BUSY errors when mixing read&write in a single transaction + if record.Collection().IsAuth() { + externalAuths, err := dao.FindAllExternalAuthsByRecord(record) + if err != nil { + return err + } + for _, auth := range externalAuths { + if err := txDao.DeleteExternalAuth(auth); err != nil { + return err + } + } + } + + // delete the record before the relation references to ensure that there + // will be no "A<->B" relations to prevent deadlock when calling DeleteRecord recursively if err := txDao.Delete(record); err != nil { return err } - // check if related records has to be deleted (if `CascadeDelete` is set) - // OR - // just unset the record id from any relation field values (if they are not required) - uniqueJsonEachAlias := "__je__" + security.PseudorandomString(4) - for refCollection, fields := range refs { - for _, field := range fields { - // fetch all referenced records + return txDao.cascadeRecordDelete(record, refs) + }) +} + +// cascadeRecordDelete triggers cascade deletion for the provided references +// and split the work to a batched set of go routines. +// +// NB! This method is expected to be called inside a transaction. +func (dao *Dao) cascadeRecordDelete(mainRecord *models.Record, refs map[*models.Collection][]*schema.SchemaField) error { + uniqueJsonEachAlias := "__je__" + security.PseudorandomString(4) + + for refCollection, fields := range refs { + for _, field := range fields { + recordTableName := inflector.Columnify(refCollection.Name) + prefixedFieldName := recordTableName + "." + inflector.Columnify(field.Name) + query := dao.RecordQuery(refCollection). + Distinct(true). + LeftJoin(fmt.Sprintf( + // note: the case is used to normalize value access for single and multiple relations. + `json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) as {{%s}}`, + prefixedFieldName, prefixedFieldName, prefixedFieldName, uniqueJsonEachAlias, + ), nil). + AndWhere(dbx.Not(dbx.HashExp{recordTableName + ".id": mainRecord.Id})). + AndWhere(dbx.HashExp{uniqueJsonEachAlias + ".value": mainRecord.Id}) + + // trigger cascade for each 1000 rel items until there is none + batchSize := 1000 + for { rows := []dbx.NullStringMap{} - recordTableName := inflector.Columnify(refCollection.Name) - prefixedFieldName := recordTableName + "." + inflector.Columnify(field.Name) - err := txDao.RecordQuery(refCollection). - Distinct(true). - LeftJoin(fmt.Sprintf( - // note: the case is used to normalize value access for single and multiple relations. - `json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) as {{%s}}`, - prefixedFieldName, prefixedFieldName, prefixedFieldName, uniqueJsonEachAlias, - ), nil). - AndWhere(dbx.Not(dbx.HashExp{recordTableName + ".id": record.Id})). - AndWhere(dbx.HashExp{uniqueJsonEachAlias + ".value": record.Id}). - All(&rows) - if err != nil { + if err := query.Limit(int64(batchSize)).All(&rows); err != nil { return err } total := len(rows) - if total == 0 { - continue + break } - ch := make(chan error) perPage := 200 pages := int(math.Ceil(float64(total) / float64(perPage))) - for i := 0; i < pages; i++ { - var chunks []dbx.NullStringMap - if len(rows) <= perPage { - chunks = rows[0:] - rows = nil - } else { - chunks = rows[0:perPage] - rows = rows[perPage:] + batchErr := func() error { + ch := make(chan error) + defer close(ch) + + for i := 0; i < pages; i++ { + var chunks []dbx.NullStringMap + if len(rows) <= perPage { + chunks = rows[0:] + rows = nil + } else { + chunks = rows[0:perPage] + rows = rows[perPage:] + } + + go func() { + refRecords := models.NewRecordsFromNullStringMaps(refCollection, chunks) + ch <- dao.deleteRefRecords(mainRecord, refRecords, field) + }() } - go func() { - refRecords := models.NewRecordsFromNullStringMaps(refCollection, chunks) - ch <- txDao.deleteRefRecords(record, refRecords, field) - }() - } - - for i := 0; i < pages; i++ { - if err := <-ch; err != nil { - close(ch) - return err + for i := 0; i < pages; i++ { + if err := <-ch; err != nil { + return err + } } + + return nil + }() + + if batchErr != nil { + return batchErr } - close(ch) - } - } - // delete linked external auths - if record.Collection().IsAuth() { - _, err = txDao.DB().Delete((&models.ExternalAuth{}).TableName(), dbx.HashExp{ - "collectionId": record.Collection().Id, - "recordId": record.Id, - }).Execute() - if err != nil { - return err + if total < batchSize { + break // no more items + } } } + } - return nil - }) + return nil } +// deleteRefRecords checks if related records has to be deleted (if `CascadeDelete` is set) +// OR +// just unset the record id from any relation field values (if they are not required). +// +// NB! This method is expected to be called inside a transaction. func (dao *Dao) deleteRefRecords(mainRecord *models.Record, refRecords []*models.Record, field *schema.SchemaField) error { options, _ := field.Options.(*schema.RelationOptions) if options == nil { @@ -492,140 +524,3 @@ func (dao *Dao) deleteRefRecords(mainRecord *models.Record, refRecords []*models return nil } - -// SyncRecordTableSchema compares the two provided collections -// and applies the necessary related record table changes. -// -// If `oldCollection` is null, then only `newCollection` is used to create the record table. -func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldCollection *models.Collection) error { - // create - if oldCollection == nil { - cols := map[string]string{ - schema.FieldNameId: "TEXT PRIMARY KEY NOT NULL", - schema.FieldNameCreated: "TEXT DEFAULT '' NOT NULL", - schema.FieldNameUpdated: "TEXT DEFAULT '' NOT NULL", - } - - if newCollection.IsAuth() { - cols[schema.FieldNameUsername] = "TEXT NOT NULL" - cols[schema.FieldNameEmail] = "TEXT DEFAULT '' NOT NULL" - cols[schema.FieldNameEmailVisibility] = "BOOLEAN DEFAULT FALSE NOT NULL" - cols[schema.FieldNameVerified] = "BOOLEAN DEFAULT FALSE NOT NULL" - cols[schema.FieldNameTokenKey] = "TEXT NOT NULL" - cols[schema.FieldNamePasswordHash] = "TEXT NOT NULL" - cols[schema.FieldNameLastResetSentAt] = "TEXT DEFAULT '' NOT NULL" - cols[schema.FieldNameLastVerificationSentAt] = "TEXT DEFAULT '' NOT NULL" - } - - // ensure that the new collection has an id - if !newCollection.HasId() { - newCollection.RefreshId() - newCollection.MarkAsNew() - } - - tableName := newCollection.Name - - // add schema field definitions - for _, field := range newCollection.Schema.Fields() { - cols[field.Name] = field.ColDefinition() - } - - // create table - if _, err := dao.DB().CreateTable(tableName, cols).Execute(); err != nil { - return err - } - - // add named index on the base `created` column - if _, err := dao.DB().CreateIndex(tableName, "_"+newCollection.Id+"_created_idx", "created").Execute(); err != nil { - return err - } - - // add named unique index on the email and tokenKey columns - if newCollection.IsAuth() { - _, err := dao.DB().NewQuery(fmt.Sprintf( - ` - CREATE UNIQUE INDEX _%s_username_idx ON {{%s}} ([[username]]); - CREATE UNIQUE INDEX _%s_email_idx ON {{%s}} ([[email]]) WHERE [[email]] != ''; - CREATE UNIQUE INDEX _%s_tokenKey_idx ON {{%s}} ([[tokenKey]]); - `, - newCollection.Id, tableName, - newCollection.Id, tableName, - newCollection.Id, tableName, - )).Execute() - if err != nil { - return err - } - } - - return nil - } - - // update - return dao.RunInTransaction(func(txDao *Dao) error { - oldTableName := oldCollection.Name - newTableName := newCollection.Name - oldSchema := oldCollection.Schema - newSchema := newCollection.Schema - - // check for renamed table - if !strings.EqualFold(oldTableName, newTableName) { - _, err := txDao.DB().RenameTable(oldTableName, newTableName).Execute() - if err != nil { - return err - } - } - - // check for deleted columns - for _, oldField := range oldSchema.Fields() { - if f := newSchema.GetFieldById(oldField.Id); f != nil { - continue // exist - } - - _, err := txDao.DB().DropColumn(newTableName, oldField.Name).Execute() - if err != nil { - return err - } - } - - // check for new or renamed columns - toRename := map[string]string{} - for _, field := range newSchema.Fields() { - oldField := oldSchema.GetFieldById(field.Id) - // Note: - // We are using a temporary column name when adding or renaming columns - // to ensure that there are no name collisions in case there is - // names switch/reuse of existing columns (eg. name, title -> title, name). - // This way we are always doing 1 more rename operation but it provides better dev experience. - - if oldField == nil { - tempName := field.Name + security.PseudorandomString(5) - toRename[tempName] = field.Name - - // add - _, err := txDao.DB().AddColumn(newTableName, tempName, field.ColDefinition()).Execute() - if err != nil { - return err - } - } else if oldField.Name != field.Name { - tempName := field.Name + security.PseudorandomString(5) - toRename[tempName] = field.Name - - // rename - _, err := txDao.DB().RenameColumn(newTableName, oldField.Name, tempName).Execute() - if err != nil { - return err - } - } - } - - // set the actual columns name - for tempName, actualName := range toRename { - _, err := txDao.DB().RenameColumn(newTableName, tempName, actualName).Execute() - if err != nil { - return err - } - } - - return nil - }) -} diff --git a/daos/record_table_sync.go b/daos/record_table_sync.go new file mode 100644 index 000000000..c0076c206 --- /dev/null +++ b/daos/record_table_sync.go @@ -0,0 +1,147 @@ +package daos + +import ( + "fmt" + "strings" + + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/security" +) + +// SyncRecordTableSchema compares the two provided collections +// and applies the necessary related record table changes. +// +// If `oldCollection` is null, then only `newCollection` is used to create the record table. +func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldCollection *models.Collection) error { + // create + if oldCollection == nil { + cols := map[string]string{ + schema.FieldNameId: "TEXT PRIMARY KEY NOT NULL", + schema.FieldNameCreated: "TEXT DEFAULT '' NOT NULL", + schema.FieldNameUpdated: "TEXT DEFAULT '' NOT NULL", + } + + if newCollection.IsAuth() { + cols[schema.FieldNameUsername] = "TEXT NOT NULL" + cols[schema.FieldNameEmail] = "TEXT DEFAULT '' NOT NULL" + cols[schema.FieldNameEmailVisibility] = "BOOLEAN DEFAULT FALSE NOT NULL" + cols[schema.FieldNameVerified] = "BOOLEAN DEFAULT FALSE NOT NULL" + cols[schema.FieldNameTokenKey] = "TEXT NOT NULL" + cols[schema.FieldNamePasswordHash] = "TEXT NOT NULL" + cols[schema.FieldNameLastResetSentAt] = "TEXT DEFAULT '' NOT NULL" + cols[schema.FieldNameLastVerificationSentAt] = "TEXT DEFAULT '' NOT NULL" + } + + // ensure that the new collection has an id + if !newCollection.HasId() { + newCollection.RefreshId() + newCollection.MarkAsNew() + } + + tableName := newCollection.Name + + // add schema field definitions + for _, field := range newCollection.Schema.Fields() { + cols[field.Name] = field.ColDefinition() + } + + // create table + if _, err := dao.DB().CreateTable(tableName, cols).Execute(); err != nil { + return err + } + + // add named index on the base `created` column + if _, err := dao.DB().CreateIndex(tableName, "_"+newCollection.Id+"_created_idx", "created").Execute(); err != nil { + return err + } + + // add named unique index on the email and tokenKey columns + if newCollection.IsAuth() { + _, err := dao.DB().NewQuery(fmt.Sprintf( + ` + CREATE UNIQUE INDEX _%s_username_idx ON {{%s}} ([[username]]); + CREATE UNIQUE INDEX _%s_email_idx ON {{%s}} ([[email]]) WHERE [[email]] != ''; + CREATE UNIQUE INDEX _%s_tokenKey_idx ON {{%s}} ([[tokenKey]]); + `, + newCollection.Id, tableName, + newCollection.Id, tableName, + newCollection.Id, tableName, + )).Execute() + if err != nil { + return err + } + } + + return nil + } + + // update + return dao.RunInTransaction(func(txDao *Dao) error { + oldTableName := oldCollection.Name + newTableName := newCollection.Name + oldSchema := oldCollection.Schema + newSchema := newCollection.Schema + + // check for renamed table + if !strings.EqualFold(oldTableName, newTableName) { + _, err := txDao.DB().RenameTable(oldTableName, newTableName).Execute() + if err != nil { + return err + } + } + + // check for deleted columns + for _, oldField := range oldSchema.Fields() { + if f := newSchema.GetFieldById(oldField.Id); f != nil { + continue // exist + } + + _, err := txDao.DB().DropColumn(newTableName, oldField.Name).Execute() + if err != nil { + return err + } + } + + // check for new or renamed columns + toRename := map[string]string{} + for _, field := range newSchema.Fields() { + oldField := oldSchema.GetFieldById(field.Id) + // Note: + // We are using a temporary column name when adding or renaming columns + // to ensure that there are no name collisions in case there is + // names switch/reuse of existing columns (eg. name, title -> title, name). + // This way we are always doing 1 more rename operation but it provides better dev experience. + + if oldField == nil { + tempName := field.Name + security.PseudorandomString(5) + toRename[tempName] = field.Name + + // add + _, err := txDao.DB().AddColumn(newTableName, tempName, field.ColDefinition()).Execute() + if err != nil { + return err + } + } else if oldField.Name != field.Name { + tempName := field.Name + security.PseudorandomString(5) + toRename[tempName] = field.Name + + // rename + _, err := txDao.DB().RenameColumn(newTableName, oldField.Name, tempName).Execute() + if err != nil { + return err + } + } + } + + // set the actual columns name + for tempName, actualName := range toRename { + _, err := txDao.DB().RenameColumn(newTableName, tempName, actualName).Execute() + if err != nil { + return err + } + } + + return nil + }) +} diff --git a/daos/record_table_sync_test.go b/daos/record_table_sync_test.go new file mode 100644 index 000000000..0e25490d4 --- /dev/null +++ b/daos/record_table_sync_test.go @@ -0,0 +1,119 @@ +package daos_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/list" +) + +func TestSyncRecordTableSchema(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + oldCollection, err := app.Dao().FindCollectionByNameOrId("demo2") + if err != nil { + t.Fatal(err) + } + updatedCollection, err := app.Dao().FindCollectionByNameOrId("demo2") + if err != nil { + t.Fatal(err) + } + updatedCollection.Name = "demo_renamed" + updatedCollection.Schema.RemoveField(updatedCollection.Schema.GetFieldByName("active").Id) + updatedCollection.Schema.AddField( + &schema.SchemaField{ + Name: "new_field", + Type: schema.FieldTypeEmail, + }, + ) + updatedCollection.Schema.AddField( + &schema.SchemaField{ + Id: updatedCollection.Schema.GetFieldByName("title").Id, + Name: "title_renamed", + Type: schema.FieldTypeEmail, + }, + ) + + scenarios := []struct { + newCollection *models.Collection + oldCollection *models.Collection + expectedTableName string + expectedColumns []string + }{ + // new base collection + { + &models.Collection{ + Name: "new_table", + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "test", + Type: schema.FieldTypeText, + }, + ), + }, + nil, + "new_table", + []string{"id", "created", "updated", "test"}, + }, + // new auth collection + { + &models.Collection{ + Name: "new_table_auth", + Type: models.CollectionTypeAuth, + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "test", + Type: schema.FieldTypeText, + }, + ), + }, + nil, + "new_table_auth", + []string{ + "id", "created", "updated", "test", + "username", "email", "verified", "emailVisibility", + "tokenKey", "passwordHash", "lastResetSentAt", "lastVerificationSentAt", + }, + }, + // no changes + { + oldCollection, + oldCollection, + "demo3", + []string{"id", "created", "updated", "title", "active"}, + }, + // renamed table, deleted column, renamed columnd and new column + { + updatedCollection, + oldCollection, + "demo_renamed", + []string{"id", "created", "updated", "title_renamed", "new_field"}, + }, + } + + for i, scenario := range scenarios { + err := app.Dao().SyncRecordTableSchema(scenario.newCollection, scenario.oldCollection) + if err != nil { + t.Errorf("(%d) %v", i, err) + continue + } + + if !app.Dao().HasTable(scenario.newCollection.Name) { + t.Errorf("(%d) Expected table %s to exist", i, scenario.newCollection.Name) + } + + cols, _ := app.Dao().GetTableColumns(scenario.newCollection.Name) + if len(cols) != len(scenario.expectedColumns) { + t.Errorf("(%d) Expected columns %v, got %v", i, scenario.expectedColumns, cols) + } + + for _, c := range cols { + if !list.ExistInSlice(c, scenario.expectedColumns) { + t.Errorf("(%d) Couldn't find column %s in %v", i, c, scenario.expectedColumns) + } + } + } +} diff --git a/daos/record_test.go b/daos/record_test.go index 2458ebd5a..c0ecdd4f8 100644 --- a/daos/record_test.go +++ b/daos/record_test.go @@ -11,10 +11,12 @@ import ( "time" "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" ) func TestRecordQuery(t *testing.T) { @@ -665,111 +667,125 @@ func TestDeleteRecord(t *testing.T) { } } -func TestSyncRecordTableSchema(t *testing.T) { +func TestDeleteRecordBatchProcessing(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() - oldCollection, err := app.Dao().FindCollectionByNameOrId("demo2") - if err != nil { + if err := createMockBatchProcessingData(app.Dao()); err != nil { t.Fatal(err) } - updatedCollection, err := app.Dao().FindCollectionByNameOrId("demo2") - if err != nil { + + // find and delete the first c1 record to trigger cascade + mainRecord, _ := app.Dao().FindRecordById("c1", "a") + if err := app.Dao().DeleteRecord(mainRecord); err != nil { t.Fatal(err) } - updatedCollection.Name = "demo_renamed" - updatedCollection.Schema.RemoveField(updatedCollection.Schema.GetFieldByName("active").Id) - updatedCollection.Schema.AddField( + + // check if the main record was deleted + _, err := app.Dao().FindRecordById(mainRecord.Collection().Id, mainRecord.Id) + if err == nil { + t.Fatal("The main record wasn't deleted") + } + + // check if the c2 rel fields were updated + c2Records, err := app.Dao().FindRecordsByExpr("c2", nil) + if err != nil || len(c2Records) == 0 { + t.Fatalf("Failed to fetch c2 records: %v", err) + } + for _, r := range c2Records { + ids := r.GetStringSlice("rel") + if len(ids) != 1 || ids[0] != "b" { + t.Fatalf("Expected only 'b' rel id, got %v", ids) + } + } + + // check if all c3 relations were deleted + c3Records, err := app.Dao().FindRecordsByExpr("c3", nil) + if err != nil { + t.Fatalf("Failed to fetch c3 records: %v", err) + } + if total := len(c3Records); total != 0 { + t.Fatalf("Expected c3 records to be deleted, found %d", total) + } +} + +func createMockBatchProcessingData(dao *daos.Dao) error { + // create mock collection without relation + c1 := &models.Collection{} + c1.Id = "c1" + c1.Name = c1.Id + c1.Schema = schema.NewSchema( &schema.SchemaField{ - Name: "new_field", - Type: schema.FieldTypeEmail, + Name: "text", + Type: schema.FieldTypeText, }, ) - updatedCollection.Schema.AddField( + if err := dao.SaveCollection(c1); err != nil { + return err + } + + // create mock collection with a multi-rel field + c2 := &models.Collection{} + c2.Id = "c2" + c2.Name = c2.Id + c2.Schema = schema.NewSchema( &schema.SchemaField{ - Id: updatedCollection.Schema.GetFieldByName("title").Id, - Name: "title_renamed", - Type: schema.FieldTypeEmail, + Name: "rel", + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{ + MaxSelect: types.Pointer(10), + CollectionId: "c1", + CascadeDelete: false, // should unset all rel fields + }, }, ) + if err := dao.SaveCollection(c2); err != nil { + return err + } - scenarios := []struct { - newCollection *models.Collection - oldCollection *models.Collection - expectedTableName string - expectedColumns []string - }{ - // new base collection - { - &models.Collection{ - Name: "new_table", - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "test", - Type: schema.FieldTypeText, - }, - ), - }, - nil, - "new_table", - []string{"id", "created", "updated", "test"}, - }, - // new auth collection - { - &models.Collection{ - Name: "new_table_auth", - Type: models.CollectionTypeAuth, - Schema: schema.NewSchema( - &schema.SchemaField{ - Name: "test", - Type: schema.FieldTypeText, - }, - ), - }, - nil, - "new_table_auth", - []string{ - "id", "created", "updated", "test", - "username", "email", "verified", "emailVisibility", - "tokenKey", "passwordHash", "lastResetSentAt", "lastVerificationSentAt", + // create mock collection with a single-rel field + c3 := &models.Collection{} + c3.Id = "c3" + c3.Name = c3.Id + c3.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "rel", + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{ + MaxSelect: types.Pointer(1), + CollectionId: "c1", + CascadeDelete: true, // should delete all c3 records }, }, - // no changes - { - oldCollection, - oldCollection, - "demo3", - []string{"id", "created", "updated", "title", "active"}, - }, - // renamed table, deleted column, renamed columnd and new column - { - updatedCollection, - oldCollection, - "demo_renamed", - []string{"id", "created", "updated", "title_renamed", "new_field"}, - }, - } - - for i, scenario := range scenarios { - err := app.Dao().SyncRecordTableSchema(scenario.newCollection, scenario.oldCollection) - if err != nil { - t.Errorf("(%d) %v", i, err) - continue - } - - if !app.Dao().HasTable(scenario.newCollection.Name) { - t.Errorf("(%d) Expected table %s to exist", i, scenario.newCollection.Name) + ) + if err := dao.SaveCollection(c3); err != nil { + return err + } + + // insert mock records + c1RecordA := models.NewRecord(c1) + c1RecordA.Id = "a" + if err := dao.Save(c1RecordA); err != nil { + return err + } + c1RecordB := models.NewRecord(c1) + c1RecordB.Id = "b" + if err := dao.Save(c1RecordB); err != nil { + return err + } + for i := 0; i < 2400; i++ { + c2Record := models.NewRecord(c2) + c2Record.Set("rel", []string{c1RecordA.Id, c1RecordB.Id}) + if err := dao.Save(c2Record); err != nil { + return err } - cols, _ := app.Dao().GetTableColumns(scenario.newCollection.Name) - if len(cols) != len(scenario.expectedColumns) { - t.Errorf("(%d) Expected columns %v, got %v", i, scenario.expectedColumns, cols) - } - - for _, c := range cols { - if !list.ExistInSlice(c, scenario.expectedColumns) { - t.Errorf("(%d) Couldn't find column %s in %v", i, c, scenario.expectedColumns) - } + c3Record := models.NewRecord(c3) + c3Record.Set("rel", c1RecordA.Id) + if err := dao.Save(c3Record); err != nil { + return err } } + + return nil } diff --git a/tools/security/random_test.go b/tools/security/random_test.go index 2130903e4..5560f9d60 100644 --- a/tools/security/random_test.go +++ b/tools/security/random_test.go @@ -31,7 +31,7 @@ func testRandomStringWithAlphabet(t *testing.T, randomFunc func(n int, alphabet expectPattern string }{ {"0123456789_", `[0-9_]+`}, - {"abcdef", `[abcdef]+`}, + {"abcdef123", `[abcdef123]+`}, {"!@#$%^&*()", `[\!\@\#\$\%\^\&\*\(\)]+`}, }