diff --git a/CHANGELOG.md b/CHANGELOG.md index fa2dbf6b..5aa27c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,11 @@ - (@todo docs) Added `record.ExpandedOne(rel)` and `record.ExpandedAll(rel)` helpers to retrieve casted single or multiple expand relations from the already loaded "expand" Record data. +## v0.16.10 + +- Added multiple valued fields (`relation`, `select`, `file`) normalizations to ensure that the zero-default value of a newly created multiple field is applied for already existing data ([#2930](https://github.com/pocketbase/pocketbase/issues/2930)). + + ## v0.16.9 - Register the `eagerRequestDataCache` middleware only for the internal `api` group routes to avoid conflicts with custom route handlers ([#2914](https://github.com/pocketbase/pocketbase/issues/2914)). diff --git a/daos/record_table_sync.go b/daos/record_table_sync.go index 7fb1504d..0cc470b8 100644 --- a/daos/record_table_sync.go +++ b/daos/record_table_sync.go @@ -174,9 +174,13 @@ func (dao *Dao) normalizeSingleVsMultipleFieldChanges(newCollection, oldCollecti return dao.RunInTransaction(func(txDao *Dao) error { for _, newField := range newCollection.Schema.Fields() { - oldField := oldCollection.Schema.GetFieldById(newField.Id) - if oldField == nil { - continue + // allow to continue even if there is no old field for the cases + // when a new field is added and there are already inserted data + var isOldMultiple bool + if oldField := oldCollection.Schema.GetFieldById(newField.Id); oldField != nil { + if opt, ok := oldField.Options.(schema.MultiValuer); ok { + isOldMultiple = opt.IsMultiple() + } } var isNewMultiple bool @@ -184,11 +188,6 @@ func (dao *Dao) normalizeSingleVsMultipleFieldChanges(newCollection, oldCollecti isNewMultiple = opt.IsMultiple() } - var isOldMultiple bool - if opt, ok := oldField.Options.(schema.MultiValuer); ok { - isOldMultiple = opt.IsMultiple() - } - if isOldMultiple == isNewMultiple { continue // no change } diff --git a/daos/record_table_sync_test.go b/daos/record_table_sync_test.go index e1155e9c..c942279a 100644 --- a/daos/record_table_sync_test.go +++ b/daos/record_table_sync_test.go @@ -171,18 +171,31 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) { 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) } 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"` + 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"` } scenarios := []struct { @@ -192,34 +205,37 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) { { "imy661ixudk5izi", expectation{ - SelectOne: `[]`, - SelectMany: ``, - FileOne: `[]`, - FileMany: ``, - RelOne: `[]`, - RelMany: ``, + SelectOne: `[]`, + SelectMany: ``, + FileOne: `[]`, + FileMany: ``, + RelOne: `[]`, + RelMany: ``, + NewMultiple: `[]`, }, }, { "al1h9ijdeojtsjy", expectation{ - SelectOne: `["optionB"]`, - SelectMany: `optionB`, - FileOne: `["300_Jsjq7RdBgA.png"]`, - FileMany: ``, - RelOne: `["84nmscqy84lsi1t"]`, - RelMany: `oap640cot4yru2s`, + SelectOne: `["optionB"]`, + SelectMany: `optionB`, + FileOne: `["300_Jsjq7RdBgA.png"]`, + FileMany: ``, + RelOne: `["84nmscqy84lsi1t"]`, + RelMany: `oap640cot4yru2s`, + NewMultiple: `[]`, }, }, { "84nmscqy84lsi1t", expectation{ - SelectOne: `["optionB"]`, - SelectMany: `optionC`, - FileOne: `["test_d61b33QdDU.txt"]`, - FileMany: `test_tC1Yc87DfC.txt`, - RelOne: `[]`, - RelMany: `oap640cot4yru2s`, + SelectOne: `["optionB"]`, + SelectMany: `optionC`, + FileOne: `["test_d61b33QdDU.txt"]`, + FileMany: `test_tC1Yc87DfC.txt`, + RelOne: `[]`, + RelMany: `oap640cot4yru2s`, + NewMultiple: `[]`, }, }, } @@ -234,6 +250,7 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) { "file_many", "rel_one", "rel_many", + "new_multiple", ).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) diff --git a/migrations/1679943780_normalize_single_multiple_values.go b/migrations/1679943780_normalize_single_multiple_values.go index 0434ebf8..bc82bc4f 100644 --- a/migrations/1679943780_normalize_single_multiple_values.go +++ b/migrations/1679943780_normalize_single_multiple_values.go @@ -12,93 +12,97 @@ import ( // 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 + return normalizeMultivaluerFields(db) }, func(db dbx.Builder) error { return nil }) } + +func normalizeMultivaluerFields(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 +} diff --git a/migrations/1689579878_renormalize_single_multiple_values.go b/migrations/1689579878_renormalize_single_multiple_values.go new file mode 100644 index 00000000..51b62b94 --- /dev/null +++ b/migrations/1689579878_renormalize_single_multiple_values.go @@ -0,0 +1,15 @@ +package migrations + +import ( + "github.com/pocketbase/dbx" +) + +// Renormalizes old single and multiple values of MultiValuer fields (file, select, relation) +// (see https://github.com/pocketbase/pocketbase/issues/2930). +func init() { + AppMigrations.Register(func(db dbx.Builder) error { + return normalizeMultivaluerFields(db) + }, func(db dbx.Builder) error { + return nil + }) +} diff --git a/resolvers/record_field_resolve_runner.go b/resolvers/record_field_resolve_runner.go index 8136b342..9ed37aa4 100644 --- a/resolvers/record_field_resolve_runner.go +++ b/resolvers/record_field_resolve_runner.go @@ -580,8 +580,8 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) { func jsonArrayLength(tableColumnPair string) string { return fmt.Sprintf( // note: the case is used to normalize value access for single and multiple relations. - `json_array_length(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END)`, - tableColumnPair, tableColumnPair, tableColumnPair, + `json_array_length(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE (CASE WHEN [[%s]] = '' OR [[%s]] IS NULL THEN json_array() ELSE json_array([[%s]]) END) END)`, + tableColumnPair, tableColumnPair, tableColumnPair, tableColumnPair, tableColumnPair, ) } diff --git a/resolvers/record_field_resolver_test.go b/resolvers/record_field_resolver_test.go index 61de222a..21e7d24b 100644 --- a/resolvers/record_field_resolver_test.go +++ b/resolvers/record_field_resolver_test.go @@ -294,14 +294,14 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) { "self_rel_one.rel_many_cascade.files:length != 7 &&" + "self_rel_one.rel_many_cascade.files:length ?!= 8", false, - "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `__data_demo4` ON [[__data_demo4.id]]={:TEST} LEFT JOIN `demo3` `__data_demo3` ON [[__data_demo3.id]] IN ({:TEST}, {:TEST}) LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_one.rel_many_cascade]]) THEN [[demo4_self_rel_one.rel_many_cascade]] ELSE json_array([[demo4_self_rel_one.rel_many_cascade]]) END) `demo4_self_rel_one_rel_many_cascade_je` LEFT JOIN `demo3` `demo4_self_rel_one_rel_many_cascade` ON [[demo4_self_rel_one_rel_many_cascade.id]] = [[demo4_self_rel_one_rel_many_cascade_je.value]] WHERE (json_array_length(CASE WHEN json_valid([[__data_demo4.self_rel_many]]) THEN [[__data_demo4.self_rel_many]] ELSE json_array([[__data_demo4.self_rel_many]]) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo4.self_rel_many]]) THEN [[__data_demo4.self_rel_many]] ELSE json_array([[__data_demo4.self_rel_many]]) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo3.files]]) THEN [[__data_demo3.files]] ELSE json_array([[__data_demo3.files]]) END) < {:TEST} AND ((json_array_length(CASE WHEN json_valid([[__data_demo3.files]]) THEN [[__data_demo3.files]] ELSE json_array([[__data_demo3.files]]) END) < {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN json_valid([[__data_mm_demo3.files]]) THEN [[__data_mm_demo3.files]] ELSE json_array([[__data_mm_demo3.files]]) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo3` `__data_mm_demo3` ON `__data_mm_demo3`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] < {:TEST})) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END) = {:TEST} AND json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END) = {:TEST} AND ((json_array_length(CASE WHEN json_valid([[demo4_self_rel_one_rel_many_cascade.files]]) THEN [[demo4_self_rel_one_rel_many_cascade.files]] ELSE json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END) != {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN json_valid([[__mm_demo4_self_rel_one_rel_many_cascade.files]]) THEN [[__mm_demo4_self_rel_one_rel_many_cascade.files]] ELSE json_array([[__mm_demo4_self_rel_one_rel_many_cascade.files]]) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo4` `__mm_demo4_self_rel_one` ON [[__mm_demo4_self_rel_one.id]] = [[__mm_demo4.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_one.rel_many_cascade]]) THEN [[__mm_demo4_self_rel_one.rel_many_cascade]] ELSE json_array([[__mm_demo4_self_rel_one.rel_many_cascade]]) END) `__mm_demo4_self_rel_one_rel_many_cascade_je` LEFT JOIN `demo3` `__mm_demo4_self_rel_one_rel_many_cascade` ON [[__mm_demo4_self_rel_one_rel_many_cascade.id]] = [[__mm_demo4_self_rel_one_rel_many_cascade_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] != {:TEST})) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND json_array_length(CASE WHEN json_valid([[demo4_self_rel_one_rel_many_cascade.files]]) THEN [[demo4_self_rel_one_rel_many_cascade.files]] ELSE json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END) != {:TEST})", + "SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `__data_demo4` ON [[__data_demo4.id]]={:TEST} LEFT JOIN `demo3` `__data_demo3` ON [[__data_demo3.id]] IN ({:TEST}, {:TEST}) LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_one.rel_many_cascade]]) THEN [[demo4_self_rel_one.rel_many_cascade]] ELSE json_array([[demo4_self_rel_one.rel_many_cascade]]) END) `demo4_self_rel_one_rel_many_cascade_je` LEFT JOIN `demo3` `demo4_self_rel_one_rel_many_cascade` ON [[demo4_self_rel_one_rel_many_cascade.id]] = [[demo4_self_rel_one_rel_many_cascade_je.value]] WHERE (json_array_length(CASE WHEN json_valid([[__data_demo4.self_rel_many]]) THEN [[__data_demo4.self_rel_many]] ELSE (CASE WHEN [[__data_demo4.self_rel_many]] = '' OR [[__data_demo4.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[__data_demo4.self_rel_many]]) END) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo4.self_rel_many]]) THEN [[__data_demo4.self_rel_many]] ELSE (CASE WHEN [[__data_demo4.self_rel_many]] = '' OR [[__data_demo4.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[__data_demo4.self_rel_many]]) END) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo3.files]]) THEN [[__data_demo3.files]] ELSE (CASE WHEN [[__data_demo3.files]] = '' OR [[__data_demo3.files]] IS NULL THEN json_array() ELSE json_array([[__data_demo3.files]]) END) END) < {:TEST} AND ((json_array_length(CASE WHEN json_valid([[__data_demo3.files]]) THEN [[__data_demo3.files]] ELSE (CASE WHEN [[__data_demo3.files]] = '' OR [[__data_demo3.files]] IS NULL THEN json_array() ELSE json_array([[__data_demo3.files]]) END) END) < {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN json_valid([[__data_mm_demo3.files]]) THEN [[__data_mm_demo3.files]] ELSE (CASE WHEN [[__data_mm_demo3.files]] = '' OR [[__data_mm_demo3.files]] IS NULL THEN json_array() ELSE json_array([[__data_mm_demo3.files]]) END) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo3` `__data_mm_demo3` ON `__data_mm_demo3`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] < {:TEST})) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[demo4_self_rel_one.self_rel_many]] = '' OR [[demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END) END) = {:TEST} AND json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[demo4_self_rel_one.self_rel_many]] = '' OR [[demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END) END) = {:TEST} AND ((json_array_length(CASE WHEN json_valid([[demo4_self_rel_one_rel_many_cascade.files]]) THEN [[demo4_self_rel_one_rel_many_cascade.files]] ELSE (CASE WHEN [[demo4_self_rel_one_rel_many_cascade.files]] = '' OR [[demo4_self_rel_one_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END) END) != {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN json_valid([[__mm_demo4_self_rel_one_rel_many_cascade.files]]) THEN [[__mm_demo4_self_rel_one_rel_many_cascade.files]] ELSE (CASE WHEN [[__mm_demo4_self_rel_one_rel_many_cascade.files]] = '' OR [[__mm_demo4_self_rel_one_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__mm_demo4_self_rel_one_rel_many_cascade.files]]) END) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo4` `__mm_demo4_self_rel_one` ON [[__mm_demo4_self_rel_one.id]] = [[__mm_demo4.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_one.rel_many_cascade]]) THEN [[__mm_demo4_self_rel_one.rel_many_cascade]] ELSE json_array([[__mm_demo4_self_rel_one.rel_many_cascade]]) END) `__mm_demo4_self_rel_one_rel_many_cascade_je` LEFT JOIN `demo3` `__mm_demo4_self_rel_one_rel_many_cascade` ON [[__mm_demo4_self_rel_one_rel_many_cascade.id]] = [[__mm_demo4_self_rel_one_rel_many_cascade_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] != {:TEST})) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND json_array_length(CASE WHEN json_valid([[demo4_self_rel_one_rel_many_cascade.files]]) THEN [[demo4_self_rel_one_rel_many_cascade.files]] ELSE (CASE WHEN [[demo4_self_rel_one_rel_many_cascade.files]] = '' OR [[demo4_self_rel_one_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END) END) != {:TEST})", }, { "json_extract and json_array_length COALESCE equal normalizations", "demo4", "json_object.a.b = '' && self_rel_many:length != 2 && json_object.a.b > 3 && self_rel_many:length <= 4", false, - "SELECT `demo4`.* FROM `demo4` WHERE ((JSON_EXTRACT([[demo4.json_object]], '$.a.b') = '' OR JSON_EXTRACT([[demo4.json_object]], '$.a.b') IS NULL) AND json_array_length(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) != {:TEST} AND JSON_EXTRACT([[demo4.json_object]], '$.a.b') > {:TEST} AND json_array_length(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) <= {:TEST})", + "SELECT `demo4`.* FROM `demo4` WHERE ((JSON_EXTRACT([[demo4.json_object]], '$.a.b') = '' OR JSON_EXTRACT([[demo4.json_object]], '$.a.b') IS NULL) AND json_array_length(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE (CASE WHEN [[demo4.self_rel_many]] = '' OR [[demo4.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4.self_rel_many]]) END) END) != {:TEST} AND JSON_EXTRACT([[demo4.json_object]], '$.a.b') > {:TEST} AND json_array_length(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE (CASE WHEN [[demo4.self_rel_many]] = '' OR [[demo4.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4.self_rel_many]]) END) END) <= {:TEST})", }, }