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 }) }