From c1edd09b357b1d553d0bd54511ceca62f536ea37 Mon Sep 17 00:00:00 2001 From: Daniel Kukula Date: Wed, 10 Dec 2025 21:32:32 +0100 Subject: [PATCH 1/3] maybe_paren_for_expr_inside_fragment --- test/ecto/adapters/myxql_test.exs | 6 ++++++ test/ecto/adapters/postgres_test.exs | 8 ++++++++ test/ecto/adapters/tds_test.exs | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/test/ecto/adapters/myxql_test.exs b/test/ecto/adapters/myxql_test.exs index 16a8e9741..34641e0d4 100644 --- a/test/ecto/adapters/myxql_test.exs +++ b/test/ecto/adapters/myxql_test.exs @@ -681,6 +681,12 @@ defmodule Ecto.Adapters.MyXQLTest do assert_raise Ecto.QueryError, fn -> all(query) end + + query = Schema |> select([r], fragment("CAST(? AS INT)", r.x and r.y)) |> plan() + assert all(query) == ~s{SELECT CAST((s0.`x` AND s0.`y`) AS INT) FROM `schema` AS s0} + + query = Schema |> select([r], fragment("CAST(? AS INT)", r.x or r.y)) |> plan() + assert all(query) == ~s{SELECT CAST((s0.`x` OR s0.`y`) AS INT) FROM `schema` AS s0} end test "literals" do diff --git a/test/ecto/adapters/postgres_test.exs b/test/ecto/adapters/postgres_test.exs index d283cafbc..052e4cd31 100644 --- a/test/ecto/adapters/postgres_test.exs +++ b/test/ecto/adapters/postgres_test.exs @@ -861,6 +861,14 @@ defmodule Ecto.Adapters.PostgresTest do assert_raise Ecto.QueryError, fn -> all(query) end + + query = Schema |> select([r], fragment("?::integer", r.x and r.y)) |> plan() + # Boolean operations inside fragments should be wrapped in parentheses + # to ensure correct precedence with surrounding SQL + assert all(query) == ~s{SELECT (s0."x" AND s0."y")::integer FROM "schema" AS s0} + + query = Schema |> select([r], fragment("?::integer", r.x or r.y)) |> plan() + assert all(query) == ~s{SELECT (s0."x" OR s0."y")::integer FROM "schema" AS s0} end test "literals" do diff --git a/test/ecto/adapters/tds_test.exs b/test/ecto/adapters/tds_test.exs index c9a01b213..0c45630f7 100644 --- a/test/ecto/adapters/tds_test.exs +++ b/test/ecto/adapters/tds_test.exs @@ -719,6 +719,12 @@ defmodule Ecto.Adapters.TdsTest do fn -> all(query) end + + query = Schema |> select([r], fragment("CAST(? AS INT)", r.x and r.y)) |> plan() + assert all(query) == ~s{SELECT CAST((s0.[x] AND s0.[y]) AS INT) FROM [schema] AS s0} + + query = Schema |> select([r], fragment("CAST(? AS INT)", r.x or r.y)) |> plan() + assert all(query) == ~s{SELECT CAST((s0.[x] OR s0.[y]) AS INT) FROM [schema] AS s0} end test "literals" do From 48ed04e6452751ac247de8f71d766908e4ce27ed Mon Sep 17 00:00:00 2001 From: Daniel Kukula Date: Wed, 10 Dec 2025 21:33:37 +0100 Subject: [PATCH 2/3] implementation --- lib/ecto/adapters/myxql/connection.ex | 2 +- lib/ecto/adapters/postgres/connection.ex | 2 +- lib/ecto/adapters/tds/connection.ex | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ecto/adapters/myxql/connection.ex b/lib/ecto/adapters/myxql/connection.ex index 1c7572aa7..ae378bebf 100644 --- a/lib/ecto/adapters/myxql/connection.ex +++ b/lib/ecto/adapters/myxql/connection.ex @@ -914,7 +914,7 @@ if Code.ensure_loaded?(MyXQL) do defp fragment_expr(parts, sources, query) do Enum.map(parts, fn {:raw, part} -> part - {:expr, expr} -> expr(expr, sources, query) + {:expr, expr} -> op_to_binary(expr, sources, query) end) |> parens_for_select() end diff --git a/lib/ecto/adapters/postgres/connection.ex b/lib/ecto/adapters/postgres/connection.ex index 983b335de..45c177cdd 100644 --- a/lib/ecto/adapters/postgres/connection.ex +++ b/lib/ecto/adapters/postgres/connection.ex @@ -1165,7 +1165,7 @@ if Code.ensure_loaded?(Postgrex) do defp fragment_expr(parts, sources, query) do Enum.map(parts, fn {:raw, part} -> part - {:expr, expr} -> expr(expr, sources, query) + {:expr, expr} -> maybe_paren(expr, sources, query) end) |> parens_for_select() end diff --git a/lib/ecto/adapters/tds/connection.ex b/lib/ecto/adapters/tds/connection.ex index 7d49c6b3d..c4053b765 100644 --- a/lib/ecto/adapters/tds/connection.ex +++ b/lib/ecto/adapters/tds/connection.ex @@ -1010,7 +1010,7 @@ if Code.ensure_loaded?(Tds) do defp fragment_expr(parts, sources, query) do Enum.map(parts, fn {:raw, part} -> part - {:expr, expr} -> expr(expr, sources, query) + {:expr, expr} -> op_to_binary(expr, sources, query) end) |> parens_for_select() end From c2422c6f949a2b47a875db8a869d0064c19e5130 Mon Sep 17 00:00:00 2001 From: Daniel Kukula Date: Wed, 10 Dec 2025 21:39:23 +0100 Subject: [PATCH 3/3] remove comment --- test/ecto/adapters/postgres_test.exs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/ecto/adapters/postgres_test.exs b/test/ecto/adapters/postgres_test.exs index 052e4cd31..cea992a37 100644 --- a/test/ecto/adapters/postgres_test.exs +++ b/test/ecto/adapters/postgres_test.exs @@ -863,8 +863,6 @@ defmodule Ecto.Adapters.PostgresTest do end query = Schema |> select([r], fragment("?::integer", r.x and r.y)) |> plan() - # Boolean operations inside fragments should be wrapped in parentheses - # to ensure correct precedence with surrounding SQL assert all(query) == ~s{SELECT (s0."x" AND s0."y")::integer FROM "schema" AS s0} query = Schema |> select([r], fragment("?::integer", r.x or r.y)) |> plan()