Skip to content

Commit 9efb8a0

Browse files
authored
fix: stabilize field order in Ash.Expr.determine_map_type/1 (#2679)
Iterating a literal map expr (e.g. `expr(%{a: id, b: name})`) produces a `{Ash.Type.Map, fields: [...]}` whose field order depends on Erlang map iteration, which for small flatmaps is driven by internal atom term ordering and varies between BEAM loads. Downstream consumers that render ordered output (e.g. ash_typescript codegen) see flaky, shuffled results across compiles. Sort the resulting fields by atom name before returning so the output is stable across compiles. The contract is only "deterministic", not "alphabetical forever" — consumers should not rely on the specific order, only that it doesn't change.
1 parent 83c62b1 commit 9efb8a0

2 files changed

Lines changed: 18 additions & 1 deletion

File tree

lib/ash/expr/expr.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1728,7 +1728,10 @@ defmodule Ash.Expr do
17281728
end)
17291729
|> case do
17301730
{:ok, fields} ->
1731-
{:ok, {Ash.Type.Map, [fields: Enum.reverse(fields)]}}
1731+
# Maps aren't ordered — sort by the atom's string name so the
1732+
# output is stable across compiles for codegen consumers.
1733+
stable_fields = Enum.sort_by(fields, fn {key, _} -> Atom.to_string(key) end)
1734+
{:ok, {Ash.Type.Map, [fields: stable_fields]}}
17321735

17331736
:error ->
17341737
:error

test/type/auto_type_test.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ defmodule Ash.Test.Type.AutoTypeTest do
212212
# map literal
213213
calculate :card, :auto, expr(%{title: title, score: score, active: active}), public?: true
214214

215+
# map literal with source keys deliberately not in alphabetic order —
216+
# used to assert deterministic (alphabetic-by-atom-name) field ordering.
217+
calculate :ordered_card, :auto, expr(%{z: title, a: score, m: active}), public?: true
218+
215219
# struct literal (Ash.Type - embedded resource)
216220
calculate :address_struct_calc,
217221
:auto,
@@ -371,6 +375,16 @@ defmodule Ash.Test.Type.AutoTypeTest do
371375
assert Keyword.get(fields, :active)[:type] == Ash.Type.Boolean
372376
end
373377

378+
test "map literal produces deterministic (alphabetic) field order" do
379+
# `:ordered_card` has source keys z, a, m — the test guards against
380+
# relying on Erlang map iteration order for small flatmaps, which is
381+
# driven by internal atom term ordering and varies between BEAM loads.
382+
calc = Ash.Resource.Info.calculation(Post, :ordered_card)
383+
fields = calc.constraints[:fields]
384+
385+
assert Keyword.keys(fields) == [:a, :m, :z]
386+
end
387+
374388
test "Ash.Type struct literal resolves to the type directly" do
375389
calc = Ash.Resource.Info.calculation(Post, :address_struct_calc)
376390
assert calc.type == Address

0 commit comments

Comments
 (0)