diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ea052f5..b87846a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,146 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline +## [v1.5.30](https://github.com/ash-project/ash_postgres/compare/v1.5.29...v1.5.30) (2024-07-12) + + + + +### Bug Fixes: + +* ensure `nil` filters aren't ever split + +* ensure we don't pass `nil` to `Ecto.Query.where` + +* remove erroneous IO.inspect + +* ensure we reselect all changing fields + +* don't use `fragment("1")` because ecto requires a proper source + +* properly use `ash_postgres_subquery` callback for `exists` on manual relationships + +* properly set tenant on aggregate queries + +* make string concat work with non literal lists + +* properly handle non-filter aggregate filters + +## [v1.5.29](https://github.com/ash-project/ash_postgres/compare/v1.5.28...v1.5.29) (2024-07-12) + + + + +### Bug Fixes: + +* ensure we don't pass `nil` to `Ecto.Query.where` + +* remove erroneous IO.inspect + +* ensure we reselect all changing fields + +* don't use `fragment("1")` because ecto requires a proper source + +* properly use `ash_postgres_subquery` callback for `exists` on manual relationships + +* properly set tenant on aggregate queries + +* make string concat work with non literal lists + +* properly handle non-filter aggregate filters + +## [v1.5.28](https://github.com/ash-project/ash_postgres/compare/v1.5.27...v1.5.28) (2024-05-28) + + + + +### Bug Fixes: + +* remove erroneous IO.inspect + +* ensure we reselect all changing fields + +* don't use `fragment("1")` because ecto requires a proper source + +* properly use `ash_postgres_subquery` callback for `exists` on manual relationships + +* properly set tenant on aggregate queries + +* make string concat work with non literal lists + +* properly handle non-filter aggregate filters + +## [v1.5.27](https://github.com/ash-project/ash_postgres/compare/v1.5.26...v1.5.27) (2024-05-28) + + + + +### Bug Fixes: + +* ensure we reselect all changing fields + +* don't use `fragment("1")` because ecto requires a proper source + +* properly use `ash_postgres_subquery` callback for `exists` on manual relationships + +* properly set tenant on aggregate queries + +* make string concat work with non literal lists + +* properly handle non-filter aggregate filters + +## [v1.5.26](https://github.com/ash-project/ash_postgres/compare/v1.5.25...v1.5.26) (2024-05-08) + + + + +### Bug Fixes: + +* don't use `fragment("1")` because ecto requires a proper source + +* properly use `ash_postgres_subquery` callback for `exists` on manual relationships + +* properly set tenant on aggregate queries + +* make string concat work with non literal lists + +* properly handle non-filter aggregate filters + +## [v1.5.25](https://github.com/ash-project/ash_postgres/compare/v1.5.24...v1.5.25) (2024-05-02) + + + + +### Bug Fixes: + +* properly use `ash_postgres_subquery` callback for `exists` on manual relationships + +* properly set tenant on aggregate queries + +* make string concat work with non literal lists + +* properly handle non-filter aggregate filters + +## [v1.5.24](https://github.com/ash-project/ash_postgres/compare/v1.5.23...v1.5.24) (2024-04-22) + + + + +### Bug Fixes: + +* make string concat work with non literal lists + +* properly handle non-filter aggregate filters + +## [v1.5.23](https://github.com/ash-project/ash_postgres/compare/v1.5.22...v1.5.23) (2024-03-28) + + + + +### Bug Fixes: + +* properly handle non-filter aggregate filters + ## [v1.5.22](https://github.com/ash-project/ash_postgres/compare/v1.5.21...v1.5.22) (2024-03-20) diff --git a/lib/aggregate.ex b/lib/aggregate.ex index a608aa6f..e8a768e5 100644 --- a/lib/aggregate.ex +++ b/lib/aggregate.ex @@ -22,6 +22,15 @@ defmodule AshPostgres.Aggregate do def add_aggregates(query, aggregates, resource, select?, source_binding, root_data) do case resource_aggregates_to_aggregates(resource, aggregates) do {:ok, aggregates} -> + tenant = + case Enum.at(aggregates, 0) do + %{context: %{tenant: tenant}} -> + tenant + + _ -> + nil + end + query = AshPostgres.DataLayer.default_bindings(query, resource) {query, aggregates} = @@ -215,7 +224,7 @@ defmodule AshPostgres.Aggregate do AshPostgres.Join.set_join_prefix( subquery, - query, + %{query | prefix: tenant}, first_relationship.destination ) else @@ -242,7 +251,7 @@ defmodule AshPostgres.Aggregate do AshPostgres.Join.set_join_prefix( subquery, - query, + %{query | prefix: tenant}, first_relationship.destination ) else @@ -266,7 +275,7 @@ defmodule AshPostgres.Aggregate do subquery = AshPostgres.Join.set_join_prefix( subquery, - query, + %{query | prefix: tenant}, first_relationship.destination ) @@ -1048,8 +1057,7 @@ defmodule AshPostgres.Aggregate do defp has_filter?(nil), do: false defp has_filter?(%{filter: nil}), do: false defp has_filter?(%{filter: %Ash.Filter{expression: nil}}), do: false - defp has_filter?(%{filter: %Ash.Filter{}}), do: true - defp has_filter?(_), do: false + defp has_filter?(_), do: true defp has_sort?(nil), do: false defp has_sort?(%{sort: nil}), do: false diff --git a/lib/data_layer.ex b/lib/data_layer.ex index d5e9fc7a..2114d326 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -371,7 +371,6 @@ defmodule AshPostgres.DataLayer do ] } - alias Ash.Filter alias Ash.Query.{BooleanExpression, Not, Ref} @behaviour Ash.DataLayer @@ -1140,12 +1139,15 @@ defmodule AshPostgres.DataLayer do Map.get(relationship, :manual) -> {module, opts} = relationship.manual - module.ash_postgres_subquery( - opts, - 0, - 0, - base_query - ) + {:ok, subquery} = + module.ash_postgres_subquery( + opts, + 0, + 0, + base_query + ) + + subquery Map.get(relationship, :no_attributes?) -> base_query @@ -2611,7 +2613,9 @@ defmodule AshPostgres.DataLayer do source = resolve_source(resource, changeset) query = from(row in source, as: ^0) |> default_bindings(resource, changeset.context) - select = Keyword.keys(changeset.atomics) ++ Ash.Resource.Info.primary_key(resource) + select = + Keyword.keys(changeset.atomics) ++ + Ash.Resource.Info.primary_key(resource) ++ Map.keys(changeset.attributes) case bulk_updatable_query(query, resource, changeset.atomics, changeset.context) do {:error, error} -> @@ -2635,9 +2639,6 @@ defmodule AshPostgres.DataLayer do {:ok, query} -> repo_opts = repo_opts(changeset.timeout, changeset.tenant, changeset.resource) - repo_opts = - Keyword.put(repo_opts, :returning, Keyword.keys(changeset.atomics)) - repo = dynamic_repo(resource, changeset) result = @@ -2660,7 +2661,7 @@ defmodule AshPostgres.DataLayer do {1, [result]} -> record = changeset.data - |> Map.merge(changeset.attributes) + |> Map.merge(Map.take(result, Map.keys(changeset.attributes))) |> Map.merge(Map.take(result, Keyword.keys(changeset.atomics))) maybe_update_tenant(resource, changeset, record) @@ -3195,6 +3196,13 @@ defmodule AshPostgres.DataLayer do |> Enum.reduce(query, fn filter, query -> {dynamic, acc} = AshPostgres.Expr.dynamic_expr(query, filter, query.__ash_bindings__) + dynamic = + if is_nil(dynamic) do + false + else + dynamic + end + query |> Ecto.Query.where(^dynamic) |> merge_expr_accumulator(acc) @@ -3202,29 +3210,37 @@ defmodule AshPostgres.DataLayer do end @doc false - def split_and_statements(%Filter{expression: expression}) do - split_and_statements(expression) + def split_and_statements(nil), do: [] + def split_and_statements(%Ash.Filter{expression: nil}), do: [] + def split_and_statements(other), do: do_split_statements(other) + + def do_split_statements(%Ash.Filter{expression: expression}) do + do_split_statements(expression) end - def split_and_statements(%BooleanExpression{op: :and, left: left, right: right}) do - split_and_statements(left) ++ split_and_statements(right) + def do_split_statements( + %Not{ + expression: %BooleanExpression{op: :or, left: left, right: right} + } + ) do + do_split_statements( + %BooleanExpression{ + op: :and, + left: %Not{expression: left}, + right: %Not{expression: right} + } + ) end - def split_and_statements(%Not{expression: %Not{expression: expression}}) do - split_and_statements(expression) + def do_split_statements(%Not{expression: %Not{expression: expression}}) do + do_split_statements(expression) end - def split_and_statements(%Not{ - expression: %BooleanExpression{op: :or, left: left, right: right} - }) do - split_and_statements(%BooleanExpression{ - op: :and, - left: %Not{expression: left}, - right: %Not{expression: right} - }) + def do_split_statements(%BooleanExpression{op: :and, left: left, right: right}) do + do_split_statements(left) ++ do_split_statements(right) end - def split_and_statements(other), do: [other] + def do_split_statements(other), do: [other] @doc false def add_binding(query, data, additional_bindings \\ 0) do diff --git a/lib/expr.ex b/lib/expr.ex index 5da45abc..f7a099e0 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -603,7 +603,8 @@ defmodule AshPostgres.Expr do embedded?, acc, type - ) do + ) + when is_list(values) do do_dynamic_expr( query, %Fragment{ @@ -620,6 +621,27 @@ defmodule AshPostgres.Expr do ) end + defp do_dynamic_expr( + query, + %StringJoin{arguments: [values, joiner], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + type + ) do + do_dynamic_expr( + query, + %Fragment{ + embedded?: pred_embedded?, + arguments: [raw: "array_to_string(", expr: values, raw: ", ", expr: joiner, raw: ")"] + }, + bindings, + embedded?, + acc, + type + ) + end + defp do_dynamic_expr( query, %StringSplit{arguments: [string, delimiter, options], embedded?: pred_embedded?}, @@ -676,7 +698,8 @@ defmodule AshPostgres.Expr do embedded?, acc, type - ) do + ) + when is_list(values) do do_dynamic_expr( query, %Fragment{ @@ -697,6 +720,24 @@ defmodule AshPostgres.Expr do ) end + defp do_dynamic_expr( + query, + %StringJoin{arguments: [values], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + type + ) do + do_dynamic_expr( + query, + %StringJoin{arguments: [values, ""], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + type + ) + end + defp do_dynamic_expr( query, %StringLength{arguments: [value], embedded?: pred_embedded?}, @@ -1520,12 +1561,33 @@ defmodule AshPostgres.Expr do on_subquery: fn subquery -> subquery = Ecto.Query.from(row in Ecto.Query.exclude(subquery, :select), - select: fragment("1") + select: row ) |> Map.put(:__ash_bindings__, subquery.__ash_bindings__) cond do Map.get(first_relationship, :manual) -> + {module, opts} = first_relationship.manual + + source_binding = + ref_binding( + %Ref{ + attribute: + Ash.Resource.Info.attribute(resource, first_relationship.source_attribute), + relationship_path: at_path, + resource: resource + }, + bindings + ) + + {:ok, subquery} = + module.ash_postgres_subquery( + opts, + source_binding, + 0, + subquery + ) + subquery Map.get(first_relationship, :no_attributes?) -> diff --git a/mix.exs b/mix.exs index 19c0d256..8e090ecb 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule AshPostgres.MixProject do support, and delegates to a configured repo. """ - @version "1.5.22" + @version "1.5.30" def project do [ diff --git a/test/calculation_test.exs b/test/calculation_test.exs index 3b6344de..412412a1 100644 --- a/test/calculation_test.exs +++ b/test/calculation_test.exs @@ -238,6 +238,23 @@ defmodule AshPostgres.CalculationTest do |> Api.read_one!() end + test "concat calculation can use a non-literal value" do + author = + Author + |> Ash.Changeset.new(%{first_name: "is", last_name: "match", badges: [:foo, :bar]}) + |> Api.create!() + + string_badges = + Author + |> Ash.Query.filter(id == ^author.id) + |> Ash.Query.calculate(:string_badges, expr(string_join(badges, ", ")), :string) + |> Api.read_one!() + |> Map.get(:calculations) + |> Map.get(:string_badges) + + assert string_badges == "foo, bar" + end + test "calculations that refer to aggregates in comparison expressions can be filtered on" do Post |> Ash.Query.load(:has_future_comment) diff --git a/test/manual_relationships_test.exs b/test/manual_relationships_test.exs index 7b1b8d23..a1956db1 100644 --- a/test/manual_relationships_test.exs +++ b/test/manual_relationships_test.exs @@ -15,6 +15,21 @@ defmodule AshPostgres.Test.ManualRelationshipsTest do Api.load!(post, :count_of_comments_containing_title) end + test "exists can be used" do + Post + |> Ash.Changeset.new(%{title: "title"}) + |> Api.create!() + + Comment + |> Ash.Changeset.new(%{title: "title2"}) + |> Api.create!() + + assert [] = + Post + |> Ash.Query.filter(exists(comments_containing_title, true)) + |> Api.read!() + end + test "aggregates can be loaded with data" do post = Post