From 6f0b685e383938faf709887c8e83e2ff3151acfc Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 5 Jan 2026 22:30:10 +0100 Subject: [PATCH] fix: handle mixed-format bucket configurations in migration (#2033) This change modifies the migration `20251001113831` to flexibly parse bucket configuration filters. This fixes this migration issue: ``` json: cannot unmarshal object into Go struct field bucketConfigurationCatchup.filter of type string ``` This occurred when a single `bucket_configuration` JSON array contained mixed formats - some buckets with old string filters and some with already-converted object filters. --- pkg/migration/20251001113831.go | 80 ++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/pkg/migration/20251001113831.go b/pkg/migration/20251001113831.go index dcfbebb00..2362420f2 100644 --- a/pkg/migration/20251001113831.go +++ b/pkg/migration/20251001113831.go @@ -17,21 +17,31 @@ package migration import ( + "encoding/json" + "src.techknowlogick.com/xormigrate" "xorm.io/xorm" - "xorm.io/xorm/schemas" ) -// Old bucket configuration format (filter as string) +// Full bucket filter struct with all fields to avoid dropping data during migration +type bucketFilterCatchup struct { + Search string `json:"s,omitempty"` + SortBy []string `json:"sort_by,omitempty"` + OrderBy []string `json:"order_by,omitempty"` + Filter string `json:"filter,omitempty"` + FilterIncludeNulls bool `json:"filter_include_nulls,omitempty"` +} + +// Flexible bucket configuration format that can handle both string and object filters type bucketConfigurationCatchup struct { - Title string `json:"title"` - Filter string `json:"filter"` + Title string `json:"title"` + Filter json.RawMessage `json:"filter"` } // New bucket configuration format (filter as object) type bucketConfigurationCatchupNew struct { - Title string `json:"title"` - Filter *taskCollection20241118123644 `json:"filter"` + Title string `json:"title"` + Filter *bucketFilterCatchup `json:"filter"` } // Old format project view @@ -61,19 +71,9 @@ func init() { Migrate: func(tx *xorm.Engine) (err error) { oldViews := []*projectViewBucketsCatchup{} - // Find views with bucket_configuration in old string format - // Only check views with bucket_configuration_mode = 2 (filter mode) - // Pattern: bucket_configuration contains "filter":"" but not "filter":{"filter": - if tx.Dialect().URI().DBType == schemas.POSTGRES { - err = tx.Where("bucket_configuration_mode = 2 AND bucket_configuration::text like '%\"filter\":\"%'"). - And("bucket_configuration::text not like '%\"filter\":{\"filter\":%'"). - Find(&oldViews) - } else { - err = tx.Where("bucket_configuration_mode = 2 AND bucket_configuration like '%\"filter\":\"%'"). - And("bucket_configuration not like '%\"filter\":{\"filter\":%'"). - Find(&oldViews) - } - + // Find all filter-mode views - we'll check individual buckets in code + err = tx.Where("bucket_configuration_mode = 2"). + Find(&oldViews) if err != nil { return } @@ -84,15 +84,43 @@ func init() { ID: view.ID, } + needsUpdate := false + // Convert each bucket configuration from old to new format for _, configuration := range view.BucketConfiguration { - newView.BucketConfiguration = append(newView.BucketConfiguration, - &bucketConfigurationCatchupNew{ - Title: configuration.Title, - Filter: &taskCollection20241118123644{ - Filter: configuration.Filter, // Wrap string in object - }, - }) + newConfig := &bucketConfigurationCatchupNew{ + Title: configuration.Title, + } + + // Check if filter is a string (old format) or object (already converted) + if len(configuration.Filter) > 0 { + switch configuration.Filter[0] { + case '"': + // It's a JSON string - extract and wrap in object + var filterString string + if err := json.Unmarshal(configuration.Filter, &filterString); err != nil { + return err + } + newConfig.Filter = &bucketFilterCatchup{ + Filter: filterString, + } + needsUpdate = true + case '{': + // It's already an object - preserve all fields + var existingFilter bucketFilterCatchup + if err := json.Unmarshal(configuration.Filter, &existingFilter); err != nil { + return err + } + newConfig.Filter = &existingFilter + } + } + + newView.BucketConfiguration = append(newView.BucketConfiguration, newConfig) + } + + // Only update if we actually found string filters to convert + if !needsUpdate { + continue } // Update only the bucket_configuration column