From 2c25f0f783100d5c07780692bfb2dd5c9334271b Mon Sep 17 00:00:00 2001 From: Daniel Shafer Date: Fri, 16 Oct 2026 15:58:24 +0000 Subject: [PATCH] Keep interpolated true filters that a later or_where depends on A runtime where/having of true was always discarded, so where: ^false, or_where: ^true became where: false, and where: ^true, or_where: ^false became or_where: false. Only skip true when it is ANDed onto existing filters, and let a filter ANDed onto a lone true replace it. Fixes #4799 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HTTGRebY77TfMpmRyQy744 --- lib/ecto/query/builder/filter.ex | 17 +++++++++---- test/ecto/query/builder/filter_test.exs | 34 ++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/lib/ecto/query/builder/filter.ex b/lib/ecto/query/builder/filter.ex index 12a374a763..5d49b67cc4 100644 --- a/lib/ecto/query/builder/filter.ex +++ b/lib/ecto/query/builder/filter.ex @@ -83,19 +83,26 @@ defmodule Ecto.Query.Builder.Filter do The callback applied by `build/4` to build the query. """ @spec apply(Ecto.Queryable.t, :where | :having, term) :: Ecto.Query.t - def apply(query, _, %{expr: true}) do - query - end def apply(%Ecto.Query{wheres: wheres} = query, :where, expr) do - %{query | wheres: wheres ++ [expr]} + %{query | wheres: add_filter(wheres, expr)} end def apply(%Ecto.Query{havings: havings} = query, :having, expr) do - %{query | havings: havings ++ [expr]} + %{query | havings: add_filter(havings, expr)} end def apply(query, kind, expr) do apply(Ecto.Queryable.to_query(query), kind, expr) end + # A runtime `true` can only be skipped when it is ANDed onto existing + # filters. On its own it must be kept, as a later `or_where`/`or_having` + # is combined with it, but a filter ANDed onto it can replace it. + defp add_filter([_ | _] = exprs, %{expr: true, op: :and}), + do: exprs + defp add_filter([%Ecto.Query.BooleanExpr{expr: true, params: [], subqueries: []}], %{op: :and} = expr), + do: [expr] + defp add_filter(exprs, expr), + do: exprs ++ [expr] + @doc """ Builds a filter based on the given arguments. diff --git a/test/ecto/query/builder/filter_test.exs b/test/ecto/query/builder/filter_test.exs index 9be7bfd603..1ca6533b68 100644 --- a/test/ecto/query/builder/filter_test.exs +++ b/test/ecto/query/builder/filter_test.exs @@ -61,7 +61,39 @@ defmodule Ecto.Query.Builder.FilterTest do describe "at runtime" do test "accepts empty keyword lists" do query = where(from(p in "posts"), [p], ^[]) - assert query.wheres == [] + assert [%{expr: true, op: :and}] = query.wheres + + query = where(from(p in "posts", where: p.id == 1), [p], ^[]) + assert [%{expr: {:==, _, _}}] = query.wheres + end + + test "keeps interpolated booleans combined with or" do + a = false + b = true + query = from(p in "posts", where: ^a, or_where: ^b) + assert [%{expr: false, op: :and}, %{expr: true, op: :or}] = query.wheres + + query = from(p in "posts", where: ^b, or_where: ^a) + assert [%{expr: true, op: :and}, %{expr: false, op: :or}] = query.wheres + + query = from(p in "posts", having: ^a, or_having: ^b) + assert [%{expr: false, op: :and}, %{expr: true, op: :or}] = query.havings + + query = from(p in "posts", having: ^b, or_having: ^a) + assert [%{expr: true, op: :and}, %{expr: false, op: :or}] = query.havings + end + + test "drops interpolated true when it is and-ed with other filters" do + bool = true + + query = from(p in "posts", where: p.id == 1, where: ^bool) + assert [%{expr: {:==, _, _}}] = query.wheres + + query = from(p in "posts", where: ^bool, where: p.id == 1) + assert [%{expr: {:==, _, _}, op: :and}] = query.wheres + + query = from(p in "posts", having: ^bool, having: p.id == 1, having: ^bool) + assert [%{expr: {:==, _, _}, op: :and}] = query.havings end test "accepts keyword lists" do