Skip to content

Commit 6ec0432

Browse files
fix(server): fix cesium_ion asset_id missing for documents with multiple legacy tile types (#149)
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 2654749 commit 6ec0432

1 file changed

Lines changed: 74 additions & 40 deletions

File tree

server/internal/infrastructure/mongo/migration/260525000715_update_tile_and_terrain_providers.go

Lines changed: 74 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ func UpdateTileAndTerrainProviders(ctx context.Context, c DBClient) error {
6464
return nil
6565
}
6666

67-
// migrateTileToCesiumIon changes tile_type to cesium_ion and adds cesium_ion_asset_id
67+
// migrateTileToCesiumIon changes tile_type to cesium_ion and adds cesium_ion_asset_id.
68+
// Uses two sequential operations. Step 2 uses a group-level filter to target only the
69+
// groups that were just converted, so documents with multiple different legacy tile types
70+
// are handled correctly (each type gets its own asset_id without cross-contamination).
6871
func migrateTileToCesiumIon(ctx context.Context, col *mongo.Collection, oldValue string, assetID int) error {
6972
filter := bson.M{
7073
"items.groups.fields.field": "tile_type",
@@ -73,56 +76,67 @@ func migrateTileToCesiumIon(ctx context.Context, col *mongo.Collection, oldValue
7376

7477
countCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
7578
defer cancel()
76-
n, err := col.CountDocuments(countCtx, filter)
79+
80+
// Collect matching document IDs for auditing before modifying
81+
cursor, err := col.Find(countCtx, filter, &options.FindOptions{
82+
Projection: bson.M{"id": 1},
83+
})
7784
if err != nil {
78-
return fmt.Errorf("count failed for tile '%s': %w", oldValue, err)
85+
return fmt.Errorf("find failed for tile '%s': %w", oldValue, err)
86+
}
87+
var matchedDocs []struct {
88+
ID string `bson:"id"`
89+
}
90+
if err := cursor.All(countCtx, &matchedDocs); err != nil {
91+
return fmt.Errorf("cursor failed for tile '%s': %w", oldValue, err)
7992
}
93+
n := int64(len(matchedDocs))
94+
8095
fmt.Printf("[migration] target documents for tile '%s': %d\n", oldValue, n)
8196
if n == 0 {
8297
fmt.Printf("[migration] nothing to do for tile '%s'\n", oldValue)
8398
return nil
8499
}
100+
for _, doc := range matchedDocs {
101+
fmt.Printf("[migration] property id=%s will be migrated: %s -> cesium_ion (asset_id=%d)\n", doc.ID, oldValue, assetID)
102+
}
85103

86-
// Step 1: Update tile_type value
87-
update := bson.M{
104+
updateCtx, cancel2 := context.WithTimeout(ctx, 30*time.Minute)
105+
defer cancel2()
106+
107+
// Step 1: Update tile_type value from oldValue to cesium_ion
108+
step1 := bson.M{
88109
"$set": bson.M{
89110
"items.$[i].groups.$[g].fields.$[f].value": "cesium_ion",
90111
},
91112
}
92-
arrayFilters := options.ArrayFilters{
113+
step1ArrayFilters := options.ArrayFilters{
93114
Filters: []interface{}{
94115
bson.M{"i.groups": bson.M{"$type": "array"}},
95116
bson.M{"g.fields": bson.M{"$type": "array"}},
96-
bson.M{
97-
"f.field": "tile_type",
98-
"f.value": oldValue,
99-
},
117+
bson.M{"f.field": "tile_type", "f.value": oldValue},
100118
},
101119
}
102-
opts := options.Update().SetArrayFilters(arrayFilters)
103-
104-
updateCtx, cancel2 := context.WithTimeout(ctx, 30*time.Minute)
105-
defer cancel2()
106-
107-
res, err := col.UpdateMany(updateCtx, filter, update, opts)
120+
res1, err := col.UpdateMany(updateCtx, filter, step1, options.Update().SetArrayFilters(step1ArrayFilters))
108121
if err != nil {
109122
return fmt.Errorf("update tile_type failed for '%s': %w", oldValue, err)
110123
}
111-
fmt.Printf("[migration] tile '%s' → cesium_ion: matched: %d, modified: %d\n", oldValue, res.MatchedCount, res.ModifiedCount)
124+
fmt.Printf("[migration] tile '%s' → cesium_ion: matched=%d modified=%d\n", oldValue, res1.MatchedCount, res1.ModifiedCount)
112125

113-
// Step 2: Add cesium_ion_asset_id field
114-
// First, check if cesium_ion_asset_id already exists to avoid duplicates
115-
filterWithoutAssetID := bson.M{
116-
"items.groups.fields": bson.M{
126+
// Step 2: Add cesium_ion_asset_id to groups that now have cesium_ion but no asset_id yet.
127+
// The group-level $[g] filter targets only groups matching BOTH conditions, so groups
128+
// that already had cesium_ion with their own asset_id are not affected.
129+
filterForAssetID := bson.M{
130+
"items.groups": bson.M{
117131
"$elemMatch": bson.M{
118-
"field": "tile_type",
119-
"value": "cesium_ion",
132+
"$and": []interface{}{
133+
bson.M{"fields": bson.M{"$elemMatch": bson.M{"field": "tile_type", "value": "cesium_ion"}}},
134+
bson.M{"fields": bson.M{"$not": bson.M{"$elemMatch": bson.M{"field": "cesium_ion_asset_id"}}}},
135+
},
120136
},
121137
},
122-
"items.groups.fields.field": bson.M{"$ne": "cesium_ion_asset_id"},
123138
}
124-
125-
updateAssetID := bson.M{
139+
step2 := bson.M{
126140
"$push": bson.M{
127141
"items.$[i].groups.$[g].fields": bson.M{
128142
"field": "cesium_ion_asset_id",
@@ -131,26 +145,22 @@ func migrateTileToCesiumIon(ctx context.Context, col *mongo.Collection, oldValue
131145
},
132146
},
133147
}
134-
arrayFiltersAssetID := options.ArrayFilters{
148+
step2ArrayFilters := options.ArrayFilters{
135149
Filters: []interface{}{
136150
bson.M{"i.groups": bson.M{"$type": "array"}},
137151
bson.M{
138-
"g.fields": bson.M{
139-
"$elemMatch": bson.M{
140-
"field": "tile_type",
141-
"value": "cesium_ion",
142-
},
152+
"$and": []interface{}{
153+
bson.M{"g.fields": bson.M{"$elemMatch": bson.M{"field": "tile_type", "value": "cesium_ion"}}},
154+
bson.M{"g.fields": bson.M{"$not": bson.M{"$elemMatch": bson.M{"field": "cesium_ion_asset_id"}}}},
143155
},
144156
},
145157
},
146158
}
147-
optsAssetID := options.Update().SetArrayFilters(arrayFiltersAssetID)
148-
149-
res2, err := col.UpdateMany(updateCtx, filterWithoutAssetID, updateAssetID, optsAssetID)
159+
res2, err := col.UpdateMany(updateCtx, filterForAssetID, step2, options.Update().SetArrayFilters(step2ArrayFilters))
150160
if err != nil {
151161
return fmt.Errorf("add cesium_ion_asset_id failed for '%s': %w", oldValue, err)
152162
}
153-
fmt.Printf("[migration] added cesium_ion_asset_id=%d: matched: %d, modified: %d\n", assetID, res2.MatchedCount, res2.ModifiedCount)
163+
fmt.Printf("[migration] added cesium_ion_asset_id=%d: matched=%d modified=%d\n", assetID, res2.MatchedCount, res2.ModifiedCount)
154164

155165
return nil
156166
}
@@ -164,15 +174,27 @@ func migrateTileSimpleRename(ctx context.Context, col *mongo.Collection, oldValu
164174

165175
countCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
166176
defer cancel()
167-
n, err := col.CountDocuments(countCtx, filter)
177+
178+
cursor, err := col.Find(countCtx, filter, &options.FindOptions{Projection: bson.M{"id": 1}})
168179
if err != nil {
169-
return fmt.Errorf("count failed for tile '%s': %w", oldValue, err)
180+
return fmt.Errorf("find failed for tile '%s': %w", oldValue, err)
181+
}
182+
var matchedDocs []struct {
183+
ID string `bson:"id"`
170184
}
185+
if err := cursor.All(countCtx, &matchedDocs); err != nil {
186+
return fmt.Errorf("cursor failed for tile '%s': %w", oldValue, err)
187+
}
188+
n := int64(len(matchedDocs))
189+
171190
fmt.Printf("[migration] target documents for tile '%s': %d\n", oldValue, n)
172191
if n == 0 {
173192
fmt.Printf("[migration] nothing to do for tile '%s'\n", oldValue)
174193
return nil
175194
}
195+
for _, doc := range matchedDocs {
196+
fmt.Printf("[migration] property id=%s will be migrated: %s -> %s\n", doc.ID, oldValue, newValue)
197+
}
176198

177199
update := bson.M{
178200
"$set": bson.M{
@@ -212,15 +234,27 @@ func migrateTerrainSimpleRename(ctx context.Context, col *mongo.Collection, oldV
212234

213235
countCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
214236
defer cancel()
215-
n, err := col.CountDocuments(countCtx, filter)
237+
238+
cursor, err := col.Find(countCtx, filter, &options.FindOptions{Projection: bson.M{"id": 1}})
216239
if err != nil {
217-
return fmt.Errorf("count failed for terrainType '%s': %w", oldValue, err)
240+
return fmt.Errorf("find failed for terrainType '%s': %w", oldValue, err)
218241
}
242+
var matchedDocs []struct {
243+
ID string `bson:"id"`
244+
}
245+
if err := cursor.All(countCtx, &matchedDocs); err != nil {
246+
return fmt.Errorf("cursor failed for terrainType '%s': %w", oldValue, err)
247+
}
248+
n := int64(len(matchedDocs))
249+
219250
fmt.Printf("[migration] target documents for terrainType '%s': %d\n", oldValue, n)
220251
if n == 0 {
221252
fmt.Printf("[migration] nothing to do for terrainType '%s'\n", oldValue)
222253
return nil
223254
}
255+
for _, doc := range matchedDocs {
256+
fmt.Printf("[migration] property id=%s will be migrated: terrainType %s -> %s\n", doc.ID, oldValue, newValue)
257+
}
224258

225259
update := bson.M{
226260
"$set": bson.M{

0 commit comments

Comments
 (0)