Skip to content

Commit

Permalink
normalized values on maxSelect change
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Mar 6, 2023
1 parent 65aa114 commit 5344ec8
Show file tree
Hide file tree
Showing 6 changed files with 430 additions and 4 deletions.
95 changes: 95 additions & 0 deletions daos/record_table_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strings"

"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/list"
Expand Down Expand Up @@ -149,10 +150,104 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
}
}

if err := txDao.normalizeSingleVsMultipleFieldChanges(newCollection, oldCollection); err != nil {
return err
}

return txDao.syncCollectionReferences(newCollection, renamedFieldNames, deletedFieldNames)
})
}

func (dao *Dao) normalizeSingleVsMultipleFieldChanges(newCollection, oldCollection *models.Collection) error {
if newCollection.IsView() || oldCollection == nil {
return nil // view or not an update
}

return dao.RunInTransaction(func(txDao *Dao) error {
for _, newField := range newCollection.Schema.Fields() {
oldField := oldCollection.Schema.GetFieldById(newField.Id)
if oldField == nil {
continue
}

var isNewMultiple bool
if opt, ok := newField.Options.(schema.MultiValuer); ok {
isNewMultiple = opt.IsMultiple()
}

var isOldMultiple bool
if opt, ok := oldField.Options.(schema.MultiValuer); ok {
isOldMultiple = opt.IsMultiple()
}

if isOldMultiple == isNewMultiple {
continue // no change
}

var updateQuery *dbx.Query

if !isOldMultiple && isNewMultiple {
// single -> multiple (convert to array)
updateQuery = txDao.DB().NewQuery(fmt.Sprintf(
`UPDATE {{%s}} set [[%s]] = (
CASE
WHEN COALESCE([[%s]], '') = ''
THEN '[]'
ELSE (
CASE
WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array'
THEN [[%s]]
ELSE json_array([[%s]])
END
)
END
)`,
newCollection.Name,
newField.Name,
newField.Name,
newField.Name,
newField.Name,
newField.Name,
newField.Name,
))
} else {
// multiple -> single (keep only the last element)
//
// note: for file fields the actual files are not deleted
// allowing additional custom handling via migration.
updateQuery = txDao.DB().NewQuery(fmt.Sprintf(
`UPDATE {{%s}} set [[%s]] = (
CASE
WHEN COALESCE([[%s]], '[]') = '[]'
THEN ''
ELSE (
CASE
WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array'
THEN COALESCE(json_extract([[%s]], '$[#-1]'), '')
ELSE [[%s]]
END
)
END
)`,
newCollection.Name,
newField.Name,
newField.Name,
newField.Name,
newField.Name,
newField.Name,
newField.Name,
))
}

if _, err := updateQuery.Execute(); err != nil {
return err
}
}

return nil
})
}

func (dao *Dao) syncCollectionReferences(collection *models.Collection, renamedFieldNames map[string]string, deletedFieldNames []string) error {
if len(renamedFieldNames) == 0 && len(deletedFieldNames) == 0 {
return nil // nothing to sync
Expand Down
133 changes: 133 additions & 0 deletions daos/record_table_sync_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
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) {
Expand Down Expand Up @@ -117,3 +121,132 @@ func TestSyncRecordTableSchema(t *testing.T) {
}
}
}

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

if err := app.Dao().SaveCollection(collection); err != nil {
t.Fatal(err)
}

type expectation 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"`
}

scenarios := []struct {
recordId string
expected expectation
}{
{
"imy661ixudk5izi",
expectation{
SelectOne: `[]`,
SelectMany: ``,
FileOne: `[]`,
FileMany: ``,
RelOne: `[]`,
RelMany: ``,
},
},
{
"al1h9ijdeojtsjy",
expectation{
SelectOne: `["optionB"]`,
SelectMany: `optionB`,
FileOne: `["300_Jsjq7RdBgA.png"]`,
FileMany: ``,
RelOne: `["84nmscqy84lsi1t"]`,
RelMany: `oap640cot4yru2s`,
},
},
{
"84nmscqy84lsi1t",
expectation{
SelectOne: `["optionB"]`,
SelectMany: `optionC`,
FileOne: `["test_d61b33QdDU.txt"]`,
FileMany: `test_tC1Yc87DfC.txt`,
RelOne: `[]`,
RelMany: `oap640cot4yru2s`,
},
},
}

for _, s := range scenarios {
result := new(expectation)

err := app.Dao().DB().Select(
"select_one",
"select_many",
"file_one",
"file_many",
"rel_one",
"rel_many",
).From(collection.Name).Where(dbx.HashExp{"id": s.recordId}).One(result)
if err != nil {
t.Errorf("[%s] Failed to load record: %v", s.recordId, err)
continue
}

encodedResult, err := json.Marshal(result)
if err != nil {
t.Errorf("[%s] Failed to encode result: %v", s.recordId, err)
continue
}

encodedExpectation, err := json.Marshal(s.expected)
if err != nil {
t.Errorf("[%s] Failed to encode expectation: %v", s.recordId, err)
continue
}

if !bytes.EqualFold(encodedExpectation, encodedResult) {
t.Errorf("[%s] Expected \n%s, \ngot \n%s", s.recordId, encodedExpectation, encodedResult)
}
}
}
104 changes: 104 additions & 0 deletions migrations/1678082970_normalize_single_multiple_values.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package migrations

import (
"fmt"

"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)

// Normalizes old single and multiple values of MultiValuer fields (file, select, relation).
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
dao := daos.New(db)

collections := []*models.Collection{}
if err := dao.CollectionQuery().All(&collections); err != nil {
return err
}

for _, c := range collections {
if c.IsView() {
// skip view collections
continue
}

for _, f := range c.Schema.Fields() {
opt, ok := f.Options.(schema.MultiValuer)
if !ok {
continue
}

var updateQuery *dbx.Query

if opt.IsMultiple() {
updateQuery = dao.DB().NewQuery(fmt.Sprintf(
`UPDATE {{%s}} set [[%s]] = (
CASE
WHEN COALESCE([[%s]], '') = ''
THEN '[]'
ELSE (
CASE
WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array'
THEN [[%s]]
ELSE json_array([[%s]])
END
)
END
)`,
c.Name,
f.Name,
f.Name,
f.Name,
f.Name,
f.Name,
f.Name,
))
} else {
updateQuery = dao.DB().NewQuery(fmt.Sprintf(
`UPDATE {{%s}} set [[%s]] = (
CASE
WHEN COALESCE([[%s]], '[]') = '[]'
THEN ''
ELSE (
CASE
WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array'
THEN COALESCE(json_extract([[%s]], '$[#-1]'), '')
ELSE [[%s]]
END
)
END
)`,
c.Name,
f.Name,
f.Name,
f.Name,
f.Name,
f.Name,
f.Name,
))
}

if _, err := updateQuery.Execute(); err != nil {
return err
}
}
}

// trigger view query update after the records normalization
// (ignore save error in case of invalid query to allow users to change it from the UI)
for _, c := range collections {
if !c.IsView() {
continue
}

dao.SaveCollection(c)
}

return nil
}, func(db dbx.Builder) error {
return nil
})
}
Loading

0 comments on commit 5344ec8

Please sign in to comment.