package daos_test import ( "bytes" "encoding/json" "testing" "github.com/pocketbase/dbx" "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 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, }, ) updatedCollection.Indexes = types.JsonArray[string]{"create index idx_title_renamed on anything (title_renamed)"} scenarios := []struct { name string newCollection *models.Collection oldCollection *models.Collection expectedColumns []string expectedIndexesCount int }{ { "new base collection", &models.Collection{ Name: "new_table", Schema: schema.NewSchema( &schema.SchemaField{ Name: "test", Type: schema.FieldTypeText, }, ), }, nil, []string{"id", "created", "updated", "test"}, 0, }, { "new auth collection", &models.Collection{ Name: "new_table_auth", Type: models.CollectionTypeAuth, Schema: schema.NewSchema( &schema.SchemaField{ Name: "test", Type: schema.FieldTypeText, }, ), Indexes: types.JsonArray[string]{"create index idx_auth_test on anything (email, username)"}, }, nil, []string{ "id", "created", "updated", "test", "username", "email", "verified", "emailVisibility", "tokenKey", "passwordHash", "lastResetSentAt", "lastVerificationSentAt", }, 4, }, { "no changes", oldCollection, oldCollection, []string{"id", "created", "updated", "title", "active"}, 3, }, { "renamed table, deleted column, renamed columnd and new column", updatedCollection, oldCollection, []string{"id", "created", "updated", "title_renamed", "new_field"}, 1, }, } for _, s := range scenarios { err := app.Dao().SyncRecordTableSchema(s.newCollection, s.oldCollection) if err != nil { t.Errorf("[%s] %v", s.name, err) continue } if !app.Dao().HasTable(s.newCollection.Name) { t.Errorf("[%s] Expected table %s to exist", s.name, s.newCollection.Name) } cols, _ := app.Dao().TableColumns(s.newCollection.Name) if len(cols) != len(s.expectedColumns) { t.Errorf("[%s] Expected columns %v, got %v", s.name, s.expectedColumns, cols) } for _, c := range cols { if !list.ExistInSlice(c, s.expectedColumns) { t.Errorf("[%s] Couldn't find column %s in %v", s.name, c, s.expectedColumns) } } indexes, _ := app.Dao().TableIndexes(s.newCollection.Name) if totalIndexes := len(indexes); totalIndexes != s.expectedIndexesCount { t.Errorf("[%s] Expected %d indexes, got %d:\n%v", s.name, s.expectedIndexesCount, totalIndexes, indexes) } } } func TestSingleVsMultipleValuesNormalization(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() collection, err := app.Dao().FindCollectionByNameOrId("demo1") if err != nil { t.Fatal(err) } // mock field changes { selectOneField := collection.Schema.GetFieldByName("select_one") opt := selectOneField.Options.(*schema.SelectOptions) opt.MaxSelect = 2 } { selectManyField := collection.Schema.GetFieldByName("select_many") opt := selectManyField.Options.(*schema.SelectOptions) opt.MaxSelect = 1 } { fileOneField := collection.Schema.GetFieldByName("file_one") opt := fileOneField.Options.(*schema.FileOptions) opt.MaxSelect = 2 } { fileManyField := collection.Schema.GetFieldByName("file_many") opt := fileManyField.Options.(*schema.FileOptions) opt.MaxSelect = 1 } { relOneField := collection.Schema.GetFieldByName("rel_one") opt := relOneField.Options.(*schema.RelationOptions) opt.MaxSelect = types.Pointer(2) } { relManyField := collection.Schema.GetFieldByName("rel_many") opt := relManyField.Options.(*schema.RelationOptions) opt.MaxSelect = types.Pointer(1) } { // new multivaluer field to check whether the array normalization // will be applied for already inserted data collection.Schema.AddField(&schema.SchemaField{ Name: "new_multiple", Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{ Values: []string{"a", "b", "c"}, MaxSelect: 3, }, }) } if err := app.Dao().SaveCollection(collection); err != nil { t.Fatal(err) } // ensures that the writable schema was reverted to its expected default var writableSchema bool app.Dao().DB().NewQuery("PRAGMA writable_schema").Row(&writableSchema) if writableSchema == true { t.Fatalf("Expected writable_schema to be OFF, got %v", writableSchema) } // check whether the columns DEFAULT definition was updated // --------------------------------------------------------------- tableInfo, err := app.Dao().TableInfo(collection.Name) if err != nil { t.Fatal(err) } tableInfoExpectations := map[string]string{ "select_one": `'[]'`, "select_many": `''`, "file_one": `'[]'`, "file_many": `''`, "rel_one": `'[]'`, "rel_many": `''`, "new_multiple": `'[]'`, } for col, dflt := range tableInfoExpectations { t.Run("check default for "+col, func(t *testing.T) { var row *models.TableInfoRow for _, r := range tableInfo { if r.Name == col { row = r break } } if row == nil { t.Fatalf("Missing info for column %q", col) } if v := row.DefaultValue.String(); v != dflt { t.Fatalf("Expected default value %q, got %q", dflt, v) } }) } // check whether the values were normalized // --------------------------------------------------------------- type fieldsExpectation struct { SelectOne string `db:"select_one"` SelectMany string `db:"select_many"` FileOne string `db:"file_one"` FileMany string `db:"file_many"` RelOne string `db:"rel_one"` RelMany string `db:"rel_many"` NewMultiple string `db:"new_multiple"` } fieldsScenarios := []struct { recordId string expected fieldsExpectation }{ { "imy661ixudk5izi", fieldsExpectation{ SelectOne: `[]`, SelectMany: ``, FileOne: `[]`, FileMany: ``, RelOne: `[]`, RelMany: ``, NewMultiple: `[]`, }, }, { "al1h9ijdeojtsjy", fieldsExpectation{ SelectOne: `["optionB"]`, SelectMany: `optionB`, FileOne: `["300_Jsjq7RdBgA.png"]`, FileMany: ``, RelOne: `["84nmscqy84lsi1t"]`, RelMany: `oap640cot4yru2s`, NewMultiple: `[]`, }, }, { "84nmscqy84lsi1t", fieldsExpectation{ SelectOne: `["optionB"]`, SelectMany: `optionC`, FileOne: `["test_d61b33QdDU.txt"]`, FileMany: `test_tC1Yc87DfC.txt`, RelOne: `[]`, RelMany: `oap640cot4yru2s`, NewMultiple: `[]`, }, }, } for _, s := range fieldsScenarios { t.Run("check fields for record "+s.recordId, func(t *testing.T) { result := new(fieldsExpectation) err := app.Dao().DB().Select( "select_one", "select_many", "file_one", "file_many", "rel_one", "rel_many", "new_multiple", ).From(collection.Name).Where(dbx.HashExp{"id": s.recordId}).One(result) if err != nil { t.Fatalf("Failed to load record: %v", err) } encodedResult, err := json.Marshal(result) if err != nil { t.Fatalf("Failed to encode result: %v", err) } encodedExpectation, err := json.Marshal(s.expected) if err != nil { t.Fatalf("Failed to encode expectation: %v", err) } if !bytes.EqualFold(encodedExpectation, encodedResult) { t.Fatalf("Expected \n%s, \ngot \n%s", encodedExpectation, encodedResult) } }) } }