From 1aeb9a2a649cfbfe1695f2e9be22f4f01cdc4a74 Mon Sep 17 00:00:00 2001 From: Onni Hakala Date: Sat, 31 Oct 2026 23:06:07 +0200 Subject: [PATCH 1/2] Store query expression files relative to the current directory Query expressions store the caller's file for error messages. With an absolute path, a copy of the project that reuses its build (git worktree, copy-on-write clone) reported the original checkout's files. Elixir's own stacktraces and diagnostics are relative to the current directory too. --- lib/ecto/query/builder.ex | 11 ++++++++++- lib/ecto/query/builder/cte.ex | 2 +- lib/ecto/query/builder/distinct.ex | 4 ++-- lib/ecto/query/builder/dynamic.ex | 2 +- lib/ecto/query/builder/filter.ex | 4 ++-- lib/ecto/query/builder/from.ex | 12 ++++++------ lib/ecto/query/builder/group_by.ex | 4 ++-- lib/ecto/query/builder/join.ex | 6 +++--- lib/ecto/query/builder/limit_offset.ex | 4 ++-- lib/ecto/query/builder/order_by.ex | 4 ++-- lib/ecto/query/builder/select.ex | 4 ++-- lib/ecto/query/builder/update.ex | 4 ++-- lib/ecto/query/builder/windows.ex | 4 ++-- test/ecto/query_test.exs | 14 ++++++++++++++ 14 files changed, 51 insertions(+), 28 deletions(-) diff --git a/lib/ecto/query/builder.ex b/lib/ecto/query/builder.ex index b81042d419..f6b75f7371 100644 --- a/lib/ecto/query/builder.ex +++ b/lib/ecto/query/builder.ex @@ -78,6 +78,15 @@ defmodule Ecto.Query.Builder do with `^index` in the query where index is a number indexing into the map. """ + @doc """ + Returns the file of the given environment to store in query expressions. + + It is relative to the current working directory when inside it, like the + paths in Elixir's stacktraces and diagnostics, so copies of a project that + reuse its build (such as git worktrees) report their own files. + """ + def file(%Macro.Env{file: file}), do: Path.relative_to_cwd(file) + @spec escape( Macro.t(), quoted_type | {:in, quoted_type} | {:out, quoted_type} | {:splice, quoted_type}, @@ -1630,7 +1639,7 @@ defmodule Ecto.Query.Builder do For example, take into account the `Builder.OrderBy`: - select = %Ecto.Query.QueryExpr{expr: expr, file: env.file, line: env.line} + select = %Ecto.Query.QueryExpr{expr: expr, file: file(env), line: env.line} Builder.apply_query(query, __MODULE__, [order_by], env) `expr` is already an escaped expression and we must not escape diff --git a/lib/ecto/query/builder/cte.ex b/lib/ecto/query/builder/cte.ex index a253fd292f..6ca14ed533 100644 --- a/lib/ecto/query/builder/cte.ex +++ b/lib/ecto/query/builder/cte.ex @@ -53,7 +53,7 @@ defmodule Ecto.Query.Builder.CTE do %Ecto.Query.QueryExpr{ expr: unquote(expr), params: unquote(params), - file: unquote(env.file), + file: unquote(Builder.file(env)), line: unquote(env.line) } end diff --git a/lib/ecto/query/builder/distinct.ex b/lib/ecto/query/builder/distinct.ex index 99e3235af9..cc3c1cdbb3 100644 --- a/lib/ecto/query/builder/distinct.ex +++ b/lib/ecto/query/builder/distinct.ex @@ -57,7 +57,7 @@ defmodule Ecto.Query.Builder.Distinct do @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t def build(query, _binding, {:^, _, [var]}, env) do quote do - Ecto.Query.Builder.Distinct.distinct!(unquote(query), unquote(var), unquote(env.file), unquote(env.line)) + Ecto.Query.Builder.Distinct.distinct!(unquote(query), unquote(var), unquote(Builder.file(env)), unquote(env.line)) end end @@ -70,7 +70,7 @@ defmodule Ecto.Query.Builder.Distinct do expr: unquote(expr), params: unquote(params), subqueries: unquote(Enum.reverse(acc.subqueries)), - file: unquote(env.file), + file: unquote(Builder.file(env)), line: unquote(env.line)} Builder.apply_query(query, __MODULE__, [distinct], env) end diff --git a/lib/ecto/query/builder/dynamic.ex b/lib/ecto/query/builder/dynamic.ex index 490f782575..4e1f6170d2 100644 --- a/lib/ecto/query/builder/dynamic.ex +++ b/lib/ecto/query/builder/dynamic.ex @@ -22,7 +22,7 @@ defmodule Ecto.Query.Builder.Dynamic do {unquote(expr), unquote(params), unquote(Enum.reverse(acc.subqueries)), unquote(aliases)} end, binding: unquote(Macro.escape(binding)), - file: unquote(env.file), + file: unquote(Builder.file(env)), line: unquote(env.line)} end end diff --git a/lib/ecto/query/builder/filter.ex b/lib/ecto/query/builder/filter.ex index 5d49b67cc4..7dab7435a2 100644 --- a/lib/ecto/query/builder/filter.ex +++ b/lib/ecto/query/builder/filter.ex @@ -58,7 +58,7 @@ defmodule Ecto.Query.Builder.Filter do def build(kind, op, query, _binding, {:^, _, [var]}, env) do quote do Ecto.Query.Builder.Filter.filter!(unquote(kind), unquote(op), unquote(query), - unquote(var), 0, unquote(env.file), unquote(env.line)) + unquote(var), 0, unquote(Builder.file(env)), unquote(env.line)) end end @@ -74,7 +74,7 @@ defmodule Ecto.Query.Builder.Filter do op: unquote(op), params: unquote(params), subqueries: unquote(subqueries), - file: unquote(env.file), + file: unquote(Builder.file(env)), line: unquote(env.line)} Builder.apply_query(query, __MODULE__, [kind, expr], env) end diff --git a/lib/ecto/query/builder/from.ex b/lib/ecto/query/builder/from.ex index e5aa708e72..a17a00cc15 100644 --- a/lib/ecto/query/builder/from.ex +++ b/lib/ecto/query/builder/from.ex @@ -103,28 +103,28 @@ defmodule Ecto.Query.Builder.From do # dependencies between modules are added source = quote(do: unquote(schema).__schema__(:source)) {:ok, prefix} = prefix || {:ok, quote(do: unquote(schema).__schema__(:prefix))} - {query(prefix, {source, schema}, [], as, hints, env.file, env.line), binds, 1} + {query(prefix, {source, schema}, [], as, hints, Builder.file(env), env.line), binds, 1} source when is_binary(source) -> {:ok, prefix} = prefix || {:ok, nil} # When a binary is used, there is no schema - {query(prefix, {source, nil}, [], as, hints, env.file, env.line), binds, 1} + {query(prefix, {source, nil}, [], as, hints, Builder.file(env), env.line), binds, 1} {source, schema} when is_binary(source) and is_atom(schema) -> {:ok, prefix} = prefix || {:ok, quote(do: unquote(schema).__schema__(:prefix))} - {query(prefix, {source, schema}, [], as, hints, env.file, env.line), binds, 1} + {query(prefix, {source, schema}, [], as, hints, Builder.file(env), env.line), binds, 1} {{:{}, _, [:fragment, _, _]} = fragment, params} -> {:ok, prefix} = prefix || {:ok, nil} - {query(prefix, fragment, params, as, hints, env.file, env.line), binds, 1} + {query(prefix, fragment, params, as, hints, Builder.file(env), env.line), binds, 1} {{{:{}, _, [:fragment, _, _]} = fragment, params}, schema} when is_atom(schema) -> {:ok, prefix} = prefix || {:ok, nil} - {query(prefix, {fragment, schema}, params, as, hints, env.file, env.line), binds, 1} + {query(prefix, {fragment, schema}, params, as, hints, Builder.file(env), env.line), binds, 1} {{:{}, _, [:values, _, _]} = values, prelude, params} -> {:ok, prefix} = prefix || {:ok, nil} - query = query(prefix, values, params, as, hints, env.file, env.line) + query = query(prefix, values, params, as, hints, Builder.file(env), env.line) quoted = quote do diff --git a/lib/ecto/query/builder/group_by.ex b/lib/ecto/query/builder/group_by.ex index 59345ac00a..42f9ff380c 100644 --- a/lib/ecto/query/builder/group_by.ex +++ b/lib/ecto/query/builder/group_by.ex @@ -93,7 +93,7 @@ defmodule Ecto.Query.Builder.GroupBy do @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t def build(query, _binding, {:^, _, [var]}, env) do quote do - Ecto.Query.Builder.GroupBy.group_by!(unquote(query), unquote(var), unquote(env.file), unquote(env.line)) + Ecto.Query.Builder.GroupBy.group_by!(unquote(query), unquote(var), unquote(Builder.file(env)), unquote(env.line)) end end @@ -106,7 +106,7 @@ defmodule Ecto.Query.Builder.GroupBy do expr: unquote(expr), params: unquote(params), subqueries: unquote(Enum.reverse(acc.subqueries)), - file: unquote(env.file), + file: unquote(Builder.file(env)), line: unquote(env.line)} Builder.apply_query(query, __MODULE__, [group_by], env) end diff --git a/lib/ecto/query/builder/join.ex b/lib/ecto/query/builder/join.ex index a238830c0c..1182990e2b 100644 --- a/lib/ecto/query/builder/join.ex +++ b/lib/ecto/query/builder/join.ex @@ -233,7 +233,7 @@ defmodule Ecto.Query.Builder.Join do join = [ as: as, assoc: join_assoc, - file: env.file, + file: Builder.file(env), line: env.line, params: join_params, prefix: prefix, @@ -266,7 +266,7 @@ defmodule Ecto.Query.Builder.Join do unquote(var), unquote(as), unquote(count_bind), - unquote(env.file), + unquote(Builder.file(env)), unquote(env.line) ) end @@ -291,7 +291,7 @@ defmodule Ecto.Query.Builder.Join do expr: unquote(on_expr), params: unquote(on_params), line: unquote(env.line), - file: unquote(env.file) + file: unquote(Builder.file(env)) } } end diff --git a/lib/ecto/query/builder/limit_offset.ex b/lib/ecto/query/builder/limit_offset.ex index 310d2a5f28..cc2052193c 100644 --- a/lib/ecto/query/builder/limit_offset.ex +++ b/lib/ecto/query/builder/limit_offset.ex @@ -54,7 +54,7 @@ defmodule Ecto.Query.Builder.LimitOffset do quote do: %Ecto.Query.LimitExpr{ expr: unquote(expr), params: unquote(params), - file: unquote(env.file), + file: unquote(Builder.file(env)), line: unquote(env.line) } end @@ -63,7 +63,7 @@ defmodule Ecto.Query.Builder.LimitOffset do quote do: %Ecto.Query.QueryExpr{ expr: unquote(expr), params: unquote(params), - file: unquote(env.file), + file: unquote(Builder.file(env)), line: unquote(env.line) } end diff --git a/lib/ecto/query/builder/order_by.ex b/lib/ecto/query/builder/order_by.ex index ff02f4436a..5f9d3638aa 100644 --- a/lib/ecto/query/builder/order_by.ex +++ b/lib/ecto/query/builder/order_by.ex @@ -239,7 +239,7 @@ defmodule Ecto.Query.Builder.OrderBy do unquote(query), unquote(var), unquote(op), - unquote(env.file), + unquote(Builder.file(env)), unquote(env.line) ) end @@ -255,7 +255,7 @@ defmodule Ecto.Query.Builder.OrderBy do expr: unquote(expr), params: unquote(params), subqueries: unquote(Enum.reverse(acc.subqueries)), - file: unquote(env.file), + file: unquote(Builder.file(env)), line: unquote(env.line) } diff --git a/lib/ecto/query/builder/select.ex b/lib/ecto/query/builder/select.ex index 960c19ae04..d01f6d92c4 100644 --- a/lib/ecto/query/builder/select.ex +++ b/lib/ecto/query/builder/select.ex @@ -310,7 +310,7 @@ defmodule Ecto.Query.Builder.Select do def build(kind, query, _binding, {:^, _, [var]}, env) do quote do Ecto.Query.Builder.Select.select!(unquote(kind), unquote(query), unquote(var), - unquote(env.file), unquote(env.line)) + unquote(Builder.file(env)), unquote(env.line)) end end @@ -324,7 +324,7 @@ defmodule Ecto.Query.Builder.Select do select = quote do: %Ecto.Query.SelectExpr{ expr: unquote(expr), params: unquote(params), - file: unquote(env.file), + file: unquote(Builder.file(env)), line: unquote(env.line), take: unquote(take), subqueries: unquote(acc.subqueries), diff --git a/lib/ecto/query/builder/update.ex b/lib/ecto/query/builder/update.ex index 29c0e69b18..f266fd4076 100644 --- a/lib/ecto/query/builder/update.ex +++ b/lib/ecto/query/builder/update.ex @@ -116,7 +116,7 @@ defmodule Ecto.Query.Builder.Update do update = quote do %Ecto.Query.QueryExpr{expr: unquote(compile), params: unquote(params), - file: unquote(env.file), line: unquote(env.line)} + file: unquote(Builder.file(env)), line: unquote(env.line)} end Builder.apply_query(query, __MODULE__, [update], env) @@ -127,7 +127,7 @@ defmodule Ecto.Query.Builder.Update do else quote do Ecto.Query.Builder.Update.update!(unquote(query), unquote(runtime), - unquote(env.file), unquote(env.line)) + unquote(Builder.file(env)), unquote(env.line)) end end end diff --git a/lib/ecto/query/builder/windows.ex b/lib/ecto/query/builder/windows.ex index ec69260a7f..53d39bfbf4 100644 --- a/lib/ecto/query/builder/windows.ex +++ b/lib/ecto/query/builder/windows.ex @@ -123,7 +123,7 @@ defmodule Ecto.Query.Builder.Windows do Ecto.Query.Builder.Windows.runtime!( unquote(query), unquote(runtime), - unquote(env.file), + unquote(Builder.file(env)), unquote(env.line) ) end @@ -149,7 +149,7 @@ defmodule Ecto.Query.Builder.Windows do expr: unquote(compile_acc), params: unquote(params), subqueries: unquote(Enum.reverse(acc.subqueries)), - file: unquote(env.file), + file: unquote(Builder.file(env)), line: unquote(env.line) } end} diff --git a/test/ecto/query_test.exs b/test/ecto/query_test.exs index 50b9cb75d5..cfdd52bd43 100644 --- a/test/ecto/query_test.exs +++ b/test/ecto/query_test.exs @@ -34,6 +34,20 @@ defmodule Ecto.QueryTest do end describe "query building" do + test "stores the file relative to the current directory" do + file = Path.relative_to_cwd(__ENV__.file) + assert Path.type(file) == :relative + + query = from p in "posts", where: p.id == 1, select: p.id, order_by: p.id + assert query.from.file == file + assert hd(query.wheres).file == file + assert query.select.file == file + assert hd(query.order_bys).file == file + + query = select("posts", [p], p.id) + assert query.select.file == file + end + test "allows macros" do test_data = "test" query = from(p in "posts") |> where([q], Macros.macro_equal(q.title, ^test_data)) From a61196737b2ff589eca4218115568e010b8e2fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 1 Nov 2026 13:14:12 +0100 Subject: [PATCH 2/2] Reorder and restore documentation for escape function --- lib/ecto/query/builder.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/ecto/query/builder.ex b/lib/ecto/query/builder.ex index f6b75f7371..77e0dc7e93 100644 --- a/lib/ecto/query/builder.ex +++ b/lib/ecto/query/builder.ex @@ -69,15 +69,6 @@ defmodule Ecto.Query.Builder do optional(any) => any } - @doc """ - Smart escapes a query expression and extracts interpolated values in - a map. - - Everything that is a query expression will be escaped, interpolated - expressions (`^foo`) will be moved to a map unescaped and replaced - with `^index` in the query where index is a number indexing into the - map. - """ @doc """ Returns the file of the given environment to store in query expressions. @@ -87,6 +78,15 @@ defmodule Ecto.Query.Builder do """ def file(%Macro.Env{file: file}), do: Path.relative_to_cwd(file) + @doc """ + Smart escapes a query expression and extracts interpolated values in + a map. + + Everything that is a query expression will be escaped, interpolated + expressions (`^foo`) will be moved to a map unescaped and replaced + with `^index` in the query where index is a number indexing into the + map. + """ @spec escape( Macro.t(), quoted_type | {:in, quoted_type} | {:out, quoted_type} | {:splice, quoted_type},