From 50a74f10dee58d040c65c7f34087c8d060e0c218 Mon Sep 17 00:00:00 2001 From: Alex Sweet Date: Fri, 21 Aug 2026 12:56:33 -0700 Subject: [PATCH] Sort escaped map/struct fields for deterministic compile output Several builder escape functions re-embed a compile-time-computed map/struct as a literal AST via bare Map.to_list/1. Map.to_list/1's field order isn't guaranteed stable across separate compiler invocations for a map that went through updates (as opposed to a fresh literal), so two builds of identical source can produce different compiled bytecode whenever any of these get folded into a literal at compile time (e.g. any from/2 call with a compile-time-known clause like limit: 1, or a select with multiple map(binding, fields) terms). --- lib/ecto/query/builder.ex | 4 ++-- lib/ecto/query/builder/select.ex | 4 ++-- lib/ecto/query/builder/windows.ex | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/ecto/query/builder.ex b/lib/ecto/query/builder.ex index f7a946308c..5d9887deac 100644 --- a/lib/ecto/query/builder.ex +++ b/lib/ecto/query/builder.ex @@ -962,7 +962,7 @@ defmodule Ecto.Query.Builder do Escape the select alias map """ @spec escape_select_aliases(map()) :: Macro.t() - def escape_select_aliases(%{} = aliases), do: {:%{}, [], Map.to_list(aliases)} + def escape_select_aliases(%{} = aliases), do: {:%{}, [], Enum.sort(Map.to_list(aliases))} @doc """ Escapes a variable according to the given binds. @@ -1684,7 +1684,7 @@ defmodule Ecto.Query.Builder do # Escapes an `Ecto.Query` and associated structs. @spec escape_query(Query.t()) :: Macro.t() - defp escape_query(%Query{} = query), do: {:%{}, [], Map.to_list(query)} + defp escape_query(%Query{} = query), do: {:%{}, [], Enum.sort(Map.to_list(query))} defp parse_access_get({{:., _, [Access, :get]}, _, [left, right]}, acc) do parse_access_get(left, [right | acc]) diff --git a/lib/ecto/query/builder/select.ex b/lib/ecto/query/builder/select.ex index 01c34ad33f..960c19ae04 100644 --- a/lib/ecto/query/builder/select.ex +++ b/lib/ecto/query/builder/select.ex @@ -318,7 +318,7 @@ defmodule Ecto.Query.Builder.Select do {query, binding} = Builder.escape_binding(query, binding, env) {expr, {params, acc}} = escape(expr, binding, env) params = Builder.escape_params(params) - take = {:%{}, [], Map.to_list(acc.take)} + take = {:%{}, [], Enum.sort(Map.to_list(acc.take))} aliases = escape_aliases(acc.aliases) select = quote do: %Ecto.Query.SelectExpr{ @@ -340,7 +340,7 @@ defmodule Ecto.Query.Builder.Select do end end - defp escape_aliases(%{} = aliases), do: {:%{}, [], Map.to_list(aliases)} + defp escape_aliases(%{} = aliases), do: {:%{}, [], Enum.sort(Map.to_list(aliases))} defp escape_aliases(aliases), do: aliases @doc """ diff --git a/lib/ecto/query/builder/windows.ex b/lib/ecto/query/builder/windows.ex index 93abd64945..ec69260a7f 100644 --- a/lib/ecto/query/builder/windows.ex +++ b/lib/ecto/query/builder/windows.ex @@ -156,7 +156,7 @@ defmodule Ecto.Query.Builder.Windows do end defp build_runtime_window({name, compile_acc, runtime_acc, params, acc}, _env) do - {:{}, [], [name, Enum.reverse(compile_acc), runtime_acc, Enum.reverse(params), {:%{}, [], Map.to_list(acc)}]} + {:{}, [], [name, Enum.reverse(compile_acc), runtime_acc, Enum.reverse(params), {:%{}, [], Enum.sort(Map.to_list(acc))}]} end @doc """