|
| 1 | +package migration |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "time" |
| 7 | + |
| 8 | + "go.mongodb.org/mongo-driver/bson" |
| 9 | + "go.mongodb.org/mongo-driver/mongo" |
| 10 | + "go.mongodb.org/mongo-driver/mongo/options" |
| 11 | +) |
| 12 | + |
| 13 | +// RevertTileAndTerrainProviders reverts the changes made by UpdateTileAndTerrainProviders |
| 14 | +// This is a recovery migration that should NOT be registered in migrations.go |
| 15 | +// Use this only when you need to rollback the UpdateTileAndTerrainProviders migration |
| 16 | +// |
| 17 | +// Tile reversion rules: |
| 18 | +// - "cesium_ion" with asset_id 2 → "default" (and remove asset_id field) |
| 19 | +// - "cesium_ion" with asset_id 3 → "default_label" (and remove asset_id field) |
| 20 | +// - "cesium_ion" with asset_id 4 → "default_road" (and remove asset_id field) |
| 21 | +// - "cesium_ion" with asset_id 3812 → "black_marble" (and remove asset_id field) |
| 22 | +// - "open_street_map" → "esri_world_topo" |
| 23 | +// Terrain reversion rules: |
| 24 | +// - "reearth_terrain" → "arcgis" |
| 25 | +func RevertTileAndTerrainProviders(ctx context.Context, c DBClient) error { |
| 26 | + col := c.WithCollection("property").Client() |
| 27 | + |
| 28 | + // Tile reversions |
| 29 | + // Reversion 1: cesium_ion (asset_id: 2) → default |
| 30 | + if err := revertCesiumIonToOldType(ctx, col, 2, "default"); err != nil { |
| 31 | + return fmt.Errorf("failed to revert cesium_ion (asset_id: 2) to 'default': %w", err) |
| 32 | + } |
| 33 | + |
| 34 | + // Reversion 2: cesium_ion (asset_id: 3) → default_label |
| 35 | + if err := revertCesiumIonToOldType(ctx, col, 3, "default_label"); err != nil { |
| 36 | + return fmt.Errorf("failed to revert cesium_ion (asset_id: 3) to 'default_label': %w", err) |
| 37 | + } |
| 38 | + |
| 39 | + // Reversion 3: cesium_ion (asset_id: 4) → default_road |
| 40 | + if err := revertCesiumIonToOldType(ctx, col, 4, "default_road"); err != nil { |
| 41 | + return fmt.Errorf("failed to revert cesium_ion (asset_id: 4) to 'default_road': %w", err) |
| 42 | + } |
| 43 | + |
| 44 | + // Reversion 4: cesium_ion (asset_id: 3812) → black_marble |
| 45 | + if err := revertCesiumIonToOldType(ctx, col, 3812, "black_marble"); err != nil { |
| 46 | + return fmt.Errorf("failed to revert cesium_ion (asset_id: 3812) to 'black_marble': %w", err) |
| 47 | + } |
| 48 | + |
| 49 | + // Reversion 5: open_street_map → esri_world_topo (simple rename) |
| 50 | + if err := revertTileSimpleRename(ctx, col, "open_street_map", "esri_world_topo"); err != nil { |
| 51 | + return fmt.Errorf("failed to revert tile 'open_street_map': %w", err) |
| 52 | + } |
| 53 | + |
| 54 | + // Terrain reversions |
| 55 | + // Reversion 7: reearth_terrain → arcgis (simple rename) |
| 56 | + if err := revertTerrainSimpleRename(ctx, col, "reearth_terrain", "arcgis"); err != nil { |
| 57 | + return fmt.Errorf("failed to revert terrain 'reearth_terrain': %w", err) |
| 58 | + } |
| 59 | + |
| 60 | + fmt.Println("[migration] RevertTileAndTerrainProviders completed successfully") |
| 61 | + return nil |
| 62 | +} |
| 63 | + |
| 64 | +// revertCesiumIonToOldType changes cesium_ion with specific asset_id back to old tile type and removes asset_id field |
| 65 | +func revertCesiumIonToOldType(ctx context.Context, col *mongo.Collection, assetID int, oldType string) error { |
| 66 | + // Find documents with cesium_ion and the specific asset_id |
| 67 | + filter := bson.M{ |
| 68 | + "items.groups.fields": bson.M{ |
| 69 | + "$elemMatch": bson.M{ |
| 70 | + "field": "tile_type", |
| 71 | + "value": "cesium_ion", |
| 72 | + }, |
| 73 | + }, |
| 74 | + "items.groups.fields.field": "cesium_ion_asset_id", |
| 75 | + "items.groups.fields.value": float64(assetID), |
| 76 | + } |
| 77 | + |
| 78 | + countCtx, cancel := context.WithTimeout(ctx, 30*time.Second) |
| 79 | + defer cancel() |
| 80 | + n, err := col.CountDocuments(countCtx, filter) |
| 81 | + if err != nil { |
| 82 | + return fmt.Errorf("count failed for cesium_ion (asset_id: %d): %w", assetID, err) |
| 83 | + } |
| 84 | + fmt.Printf("[migration] target documents for cesium_ion (asset_id: %d): %d\n", assetID, n) |
| 85 | + if n == 0 { |
| 86 | + fmt.Printf("[migration] nothing to do for cesium_ion (asset_id: %d)\n", assetID) |
| 87 | + return nil |
| 88 | + } |
| 89 | + |
| 90 | + updateCtx, cancel2 := context.WithTimeout(ctx, 30*time.Minute) |
| 91 | + defer cancel2() |
| 92 | + |
| 93 | + // Step 1: Update tile_type value from cesium_ion to old type |
| 94 | + update := bson.M{ |
| 95 | + "$set": bson.M{ |
| 96 | + "items.$[i].groups.$[g].fields.$[f].value": oldType, |
| 97 | + }, |
| 98 | + } |
| 99 | + arrayFilters := options.ArrayFilters{ |
| 100 | + Filters: []interface{}{ |
| 101 | + bson.M{"i.groups": bson.M{"$type": "array"}}, |
| 102 | + bson.M{ |
| 103 | + "g.fields": bson.M{ |
| 104 | + "$elemMatch": bson.M{ |
| 105 | + "field": "cesium_ion_asset_id", |
| 106 | + "value": float64(assetID), |
| 107 | + }, |
| 108 | + }, |
| 109 | + }, |
| 110 | + bson.M{ |
| 111 | + "f.field": "tile_type", |
| 112 | + "f.value": "cesium_ion", |
| 113 | + }, |
| 114 | + }, |
| 115 | + } |
| 116 | + opts := options.Update().SetArrayFilters(arrayFilters) |
| 117 | + |
| 118 | + res, err := col.UpdateMany(updateCtx, filter, update, opts) |
| 119 | + if err != nil { |
| 120 | + return fmt.Errorf("update tile_type failed for cesium_ion (asset_id: %d): %w", assetID, err) |
| 121 | + } |
| 122 | + fmt.Printf("[migration] cesium_ion (asset_id: %d) → '%s': matched: %d, modified: %d\n", assetID, oldType, res.MatchedCount, res.ModifiedCount) |
| 123 | + |
| 124 | + // Step 2: Remove cesium_ion_asset_id field |
| 125 | + filterForRemoval := bson.M{ |
| 126 | + "$and": bson.A{ |
| 127 | + bson.M{ |
| 128 | + "items.groups.fields": bson.M{ |
| 129 | + "$elemMatch": bson.M{ |
| 130 | + "field": "tile_type", |
| 131 | + "value": oldType, |
| 132 | + }, |
| 133 | + }, |
| 134 | + }, |
| 135 | + bson.M{ |
| 136 | + "items.groups.fields": bson.M{ |
| 137 | + "$elemMatch": bson.M{ |
| 138 | + "field": "cesium_ion_asset_id", |
| 139 | + "value": float64(assetID), |
| 140 | + }, |
| 141 | + }, |
| 142 | + }, |
| 143 | + }, |
| 144 | + } |
| 145 | + |
| 146 | + updateRemoval := bson.M{ |
| 147 | + "$pull": bson.M{ |
| 148 | + "items.$[i].groups.$[g].fields": bson.M{ |
| 149 | + "field": "cesium_ion_asset_id", |
| 150 | + "value": float64(assetID), |
| 151 | + }, |
| 152 | + }, |
| 153 | + } |
| 154 | + arrayFiltersRemoval := options.ArrayFilters{ |
| 155 | + Filters: []interface{}{ |
| 156 | + bson.M{"i.groups": bson.M{"$type": "array"}}, |
| 157 | + bson.M{ |
| 158 | + "g.fields": bson.M{ |
| 159 | + "$elemMatch": bson.M{ |
| 160 | + "field": "cesium_ion_asset_id", |
| 161 | + "value": float64(assetID), |
| 162 | + }, |
| 163 | + }, |
| 164 | + }, |
| 165 | + }, |
| 166 | + } |
| 167 | + optsRemoval := options.Update().SetArrayFilters(arrayFiltersRemoval) |
| 168 | + |
| 169 | + res2, err := col.UpdateMany(updateCtx, filterForRemoval, updateRemoval, optsRemoval) |
| 170 | + if err != nil { |
| 171 | + return fmt.Errorf("remove cesium_ion_asset_id failed for asset_id %d: %w", assetID, err) |
| 172 | + } |
| 173 | + fmt.Printf("[migration] removed cesium_ion_asset_id=%d: matched: %d, modified: %d\n", assetID, res2.MatchedCount, res2.ModifiedCount) |
| 174 | + |
| 175 | + return nil |
| 176 | +} |
| 177 | + |
| 178 | +// revertTileSimpleRename changes one tile_type value back to another |
| 179 | +func revertTileSimpleRename(ctx context.Context, col *mongo.Collection, currentValue, oldValue string) error { |
| 180 | + filter := bson.M{ |
| 181 | + "items.groups.fields.field": "tile_type", |
| 182 | + "items.groups.fields.value": currentValue, |
| 183 | + } |
| 184 | + |
| 185 | + countCtx, cancel := context.WithTimeout(ctx, 30*time.Second) |
| 186 | + defer cancel() |
| 187 | + n, err := col.CountDocuments(countCtx, filter) |
| 188 | + if err != nil { |
| 189 | + return fmt.Errorf("count failed for tile '%s': %w", currentValue, err) |
| 190 | + } |
| 191 | + fmt.Printf("[migration] target documents for tile '%s': %d\n", currentValue, n) |
| 192 | + if n == 0 { |
| 193 | + fmt.Printf("[migration] nothing to do for tile '%s'\n", currentValue) |
| 194 | + return nil |
| 195 | + } |
| 196 | + |
| 197 | + update := bson.M{ |
| 198 | + "$set": bson.M{ |
| 199 | + "items.$[i].groups.$[g].fields.$[f].value": oldValue, |
| 200 | + }, |
| 201 | + } |
| 202 | + arrayFilters := options.ArrayFilters{ |
| 203 | + Filters: []interface{}{ |
| 204 | + bson.M{"i.groups": bson.M{"$type": "array"}}, |
| 205 | + bson.M{"g.fields": bson.M{"$type": "array"}}, |
| 206 | + bson.M{ |
| 207 | + "f.field": "tile_type", |
| 208 | + "f.value": currentValue, |
| 209 | + }, |
| 210 | + }, |
| 211 | + } |
| 212 | + opts := options.Update().SetArrayFilters(arrayFilters) |
| 213 | + |
| 214 | + updateCtx, cancel2 := context.WithTimeout(ctx, 30*time.Minute) |
| 215 | + defer cancel2() |
| 216 | + |
| 217 | + res, err := col.UpdateMany(updateCtx, filter, update, opts) |
| 218 | + if err != nil { |
| 219 | + return fmt.Errorf("update failed for tile '%s': %w", currentValue, err) |
| 220 | + } |
| 221 | + |
| 222 | + fmt.Printf("[migration] tile '%s' → '%s': matched: %d, modified: %d\n", currentValue, oldValue, res.MatchedCount, res.ModifiedCount) |
| 223 | + return nil |
| 224 | +} |
| 225 | + |
| 226 | +// revertTerrainSimpleRename changes one terrainType value back to another |
| 227 | +func revertTerrainSimpleRename(ctx context.Context, col *mongo.Collection, currentValue, oldValue string) error { |
| 228 | + filter := bson.M{ |
| 229 | + "items.groups.fields.field": "terrainType", |
| 230 | + "items.groups.fields.value": currentValue, |
| 231 | + } |
| 232 | + |
| 233 | + countCtx, cancel := context.WithTimeout(ctx, 30*time.Second) |
| 234 | + defer cancel() |
| 235 | + n, err := col.CountDocuments(countCtx, filter) |
| 236 | + if err != nil { |
| 237 | + return fmt.Errorf("count failed for terrainType '%s': %w", currentValue, err) |
| 238 | + } |
| 239 | + fmt.Printf("[migration] target documents for terrainType '%s': %d\n", currentValue, n) |
| 240 | + if n == 0 { |
| 241 | + fmt.Printf("[migration] nothing to do for terrainType '%s'\n", currentValue) |
| 242 | + return nil |
| 243 | + } |
| 244 | + |
| 245 | + update := bson.M{ |
| 246 | + "$set": bson.M{ |
| 247 | + "items.$[i].groups.$[g].fields.$[f].value": oldValue, |
| 248 | + }, |
| 249 | + } |
| 250 | + arrayFilters := options.ArrayFilters{ |
| 251 | + Filters: []interface{}{ |
| 252 | + bson.M{"i.groups": bson.M{"$type": "array"}}, |
| 253 | + bson.M{"g.fields": bson.M{"$type": "array"}}, |
| 254 | + bson.M{ |
| 255 | + "f.field": "terrainType", |
| 256 | + "f.value": currentValue, |
| 257 | + }, |
| 258 | + }, |
| 259 | + } |
| 260 | + opts := options.Update().SetArrayFilters(arrayFilters) |
| 261 | + |
| 262 | + updateCtx, cancel2 := context.WithTimeout(ctx, 30*time.Minute) |
| 263 | + defer cancel2() |
| 264 | + |
| 265 | + res, err := col.UpdateMany(updateCtx, filter, update, opts) |
| 266 | + if err != nil { |
| 267 | + return fmt.Errorf("update failed for terrainType '%s': %w", currentValue, err) |
| 268 | + } |
| 269 | + |
| 270 | + fmt.Printf("[migration] terrainType '%s' → '%s': matched: %d, modified: %d\n", currentValue, oldValue, res.MatchedCount, res.ModifiedCount) |
| 271 | + return nil |
| 272 | +} |
0 commit comments