diff --git a/daos/record_expand.go b/daos/record_expand.go index fd551869..ec8881be 100644 --- a/daos/record_expand.go +++ b/daos/record_expand.go @@ -9,6 +9,7 @@ import ( "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/dbutils" "github.com/pocketbase/pocketbase/tools/inflector" "github.com/pocketbase/pocketbase/tools/list" "github.com/pocketbase/pocketbase/tools/security" @@ -86,7 +87,7 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch if indirectRelFieldOptions == nil || indirectRelFieldOptions.CollectionId != mainCollection.Id { return fmt.Errorf("Invalid indirect relation field path %q.", parts[0]) } - if indirectRelFieldOptions.MaxSelect != nil && *indirectRelFieldOptions.MaxSelect != 1 { + if indirectRelFieldOptions.IsMultiple() { // for now don't allow multi-relation indirect fields expand // due to eventual poor query performance with large data sets. return fmt.Errorf("Multi-relation fields cannot be indirectly expanded in %q.", parts[0]) @@ -126,7 +127,7 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch MaxSelect: nil, CollectionId: indirectRel.Id, } - if indirectRelField.Unique { + if isRelFieldUnique(indirectRel, indirectRelField.Name) { relFieldOptions.MaxSelect = types.Pointer(1) } // indirect relation @@ -269,3 +270,14 @@ func normalizeExpands(paths []string) []string { return list.ToUniqueStringSlice(result) } + +func isRelFieldUnique(collection *models.Collection, fieldName string) bool { + for _, idx := range collection.Indexes { + parsed := dbutils.ParseIndex(idx.(string)) + if parsed.Unique && len(parsed.Columns) == 1 && strings.EqualFold(parsed.Columns[0].Name, fieldName) { + return true + } + } + + return false +} diff --git a/daos/record_expand_test.go b/daos/record_expand_test.go index f7fdf6e9..d37294d5 100644 --- a/daos/record_expand_test.go +++ b/daos/record_expand_test.go @@ -362,3 +362,57 @@ func TestExpandRecord(t *testing.T) { } } } + +func TestIndirectExpandSingeVsArrayResult(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + record, err := app.Dao().FindRecordById("demo3", "7nwo8tuiatetxdm") + if err != nil { + t.Fatal(err) + } + + // non-unique indirect expand + { + errs := app.Dao().ExpandRecord(record, []string{"demo4(rel_one_cascade)"}, func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c.Id, ids, nil) + }) + if len(errs) > 0 { + t.Fatal(errs) + } + + result, ok := record.Expand()["demo4(rel_one_cascade)"].([]*models.Record) + if !ok { + t.Fatalf("Expected the expanded result to be a slice, got %v", result) + } + } + + // mock a unique constraint for the rel_one_cascade field + { + demo4, err := app.Dao().FindCollectionByNameOrId("demo4") + if err != nil { + t.Fatal(err) + } + + demo4.Indexes = append(demo4.Indexes, "create unique index idx_unique_expand on demo4 (rel_one_cascade)") + + if err := app.Dao().SaveCollection(demo4); err != nil { + t.Fatalf("Failed to mock unique constraint: %v", err) + } + } + + // non-unique indirect expand + { + errs := app.Dao().ExpandRecord(record, []string{"demo4(rel_one_cascade)"}, func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c.Id, ids, nil) + }) + if len(errs) > 0 { + t.Fatal(errs) + } + + result, ok := record.Expand()["demo4(rel_one_cascade)"].(*models.Record) + if !ok { + t.Fatalf("Expected the expanded result to be a single model, got %v", result) + } + } +} diff --git a/tests/data/logs.db b/tests/data/logs.db index 0dc93ab0..79b96f2e 100644 Binary files a/tests/data/logs.db and b/tests/data/logs.db differ