@@ -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).
6871func 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