Skip to content

Keep generated Dict and SeqDict migrations valid when nested - #111

Open
CharlonTank wants to merge 1 commit into
lamdera:lamdera-nextfrom
CharlonTank:fix/migration-dict-map-nesting
Open

CharlonTank wants to merge 1 commit into
lamdera:lamdera-nextfrom
CharlonTank:fix/migration-dict-map-nesting

Conversation

@CharlonTank

@CharlonTank CharlonTank commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Two of the Dict / SeqDict migration templates are only valid at the top level of a field migration:

  1. Value migrations are emitted as Dict.map (\k -> <m>). When the value is itself a Dict/SeqDict that needs a migration, the output is

    old.x |> Dict.map (\k -> Dict.map (\k -> migrate_UserType))

    which Elm rejects with a SHADOWING error. The existing Migrate_All/Expected.elm snapshot already contains exactly this (nestedDictCustomType); it just never gets compiled.

  2. Key migrations are emitted as a pipeline fragment, SeqDict.toList |> List.map (…) |> SeqDict.fromList. That works after old.field |>, but as soon as it is nested in a function position (Dict.map (\k -> …), Maybe.map (…), …) it is no longer a function. For example, Dict String (SeqDict CustomKey v) generates a migration that fails to type check.

We hit both in almost every production migration of our app that touches such a field, and had to fix them by hand each time.

Fix

  • Bind \_ instead of \k. The key is never used, so nesting can no longer shadow anything.
  • Compose with >> instead of piping with |>, as the Result template already does. At the top level, old.x |> SeqDict.toList >> List.map (…) >> SeqDict.fromList stays valid because >> binds tighter than |>.
  • Parenthesise the Tuple.mapFirst argument, as Tuple.mapBoth already does, so a compound key migration stays a single argument. elm-format removes the parentheses when they are redundant.

Tests

  • The Migrate_All scenario gets a new field, nestedSeqDictCustomKey : Dict String (SeqDict UserType Int), which adds lamdera/containers to the scenario's elm.json. The snapshots are regenerated.
  • migration-scenarios is pending, so I ran it locally with testExamples on src/Migrate_All only: ✅ 2 tests passed. The Migrate_External_Paramed scenario currently throws first, on a Wire3 encoder type error while compiling the scenario itself, which is unrelated to this change.
  • I compiled the regenerated Expected.elm with lamdera make. Before the change it fails with SHADOWING on nestedDictCustomType. After it, the only errors left are the intentional Unimplemented placeholders.

Related

Value migrations were emitted as `Dict.map (\k -> m)`: a value that is
itself a migrated Dict produced `Dict.map (\k -> Dict.map (\k -> m))`,
rejected by Elm (SHADOWING). Key migrations were emitted as the pipeline
`Dict.toList |> List.map (..) |> Dict.fromList`, which is not a function
once nested (e.g. `Dict String (SeqDict CustomKey v)`), so the migration
failed to type check.

Use `\_` and compose with `>>` like the Result template does; this stays
valid at the top level (`old.x |> Dict.toList >> ..`).
CharlonTank added a commit to CharlonTank/compiler that referenced this pull request Sep 25, 2026
… nested

Same templates as Dict and SeqDict, same two problems (see lamdera#111): a nested
value migration shadowed its `\k`, and a key migration emitted as a `|>`
pipeline is not a function once nested. Use `\_` and compose with `>>`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant