From 31d0ec20ac8d1e24f4cbafd2ff558f1311b6fbab Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Tue, 20 Sep 2022 23:00:29 -0400 Subject: [PATCH] improvement: support latest ash --- .vscode/settings.json | 5 --- lib/expr.ex | 75 ++++++++++++++++++++++++++++++++------- mix.exs | 2 +- mix.lock | 4 +-- test/filter_test.exs | 18 ++++++++++ test/support/test_repo.ex | 3 +- 6 files changed, 86 insertions(+), 21 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e42081c..3f9c00f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,4 @@ { - "workbench.colorCustomizations": { - "activityBar.background": "#113421", - "titleBar.activeBackground": "#18482E", - "titleBar.activeForeground": "#F4FBF7" - }, "cSpell.words": [ "citext", "mapset", diff --git a/lib/expr.ex b/lib/expr.ex index fbd983e..e78a67a 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -521,9 +521,17 @@ defmodule AshPostgres.Expr do do_dynamic_expr(query, Enum.to_list(mapset), bindings, embedded?, type) end - defp do_dynamic_expr(query, %Ash.CiString{string: string}, bindings, embedded?, type) do + defp do_dynamic_expr( + query, + %Ash.CiString{string: string} = expression, + bindings, + embedded?, + type + ) do string = do_dynamic_expr(query, string, bindings, embedded?) + require_extension!(query, "citext", expression) + do_dynamic_expr( query, %Fragment{ @@ -546,13 +554,14 @@ defmodule AshPostgres.Expr do attribute: %Ash.Query.Calculation{} = calculation, relationship_path: [], resource: resource - }, + } = type_expr, bindings, embedded?, _type ) do calculation = %{calculation | load: calculation.name} type = AshPostgres.Types.parameterized_type(calculation.type, []) + validate_type!(query, type, type_expr) case Ash.Filter.hydrate_refs( calculation.module.expression(calculation.opts, calculation.context), @@ -581,7 +590,7 @@ defmodule AshPostgres.Expr do end defp do_dynamic_expr( - _query, + query, %Ref{attribute: %Ash.Query.Aggregate{} = aggregate} = ref, bindings, _embedded?, @@ -596,6 +605,7 @@ defmodule AshPostgres.Expr do expr = Ecto.Query.dynamic(field(as(^ref_binding), ^aggregate.name)) type = AshPostgres.Types.parameterized_type(aggregate.type, []) + validate_type!(query, type, ref) type = if type && aggregate.kind == :list do @@ -646,6 +656,8 @@ defmodule AshPostgres.Expr do type = AshPostgres.Types.parameterized_type(calculation.type, []) + validate_type!(query, type, ref) + case Ash.Filter.hydrate_refs( calculation.module.expression(calculation.opts, calculation.context), %{ @@ -676,7 +688,7 @@ defmodule AshPostgres.Expr do defp do_dynamic_expr( query, - %Type{arguments: [arg1, arg2], embedded?: pred_embedded?}, + %Type{arguments: [arg1, arg2], embedded?: pred_embedded?} = type_expr, bindings, embedded?, _type @@ -684,6 +696,7 @@ defmodule AshPostgres.Expr do arg1 = do_dynamic_expr(query, arg1, bindings, false) arg2 = do_dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?) type = AshPostgres.Types.parameterized_type(arg2, []) + validate_type!(query, type, type_expr) if type do Ecto.Query.dynamic(type(^arg1, ^type)) @@ -694,7 +707,7 @@ defmodule AshPostgres.Expr do defp do_dynamic_expr( query, - %Type{arguments: [arg1, arg2, constraints], embedded?: pred_embedded?}, + %Type{arguments: [arg1, arg2, constraints], embedded?: pred_embedded?} = type_expr, bindings, embedded?, _type @@ -702,6 +715,7 @@ defmodule AshPostgres.Expr do arg1 = do_dynamic_expr(query, arg1, bindings, false) arg2 = do_dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?) type = AshPostgres.Types.parameterized_type(arg2, constraints) + validate_type!(query, type, type_expr) if type do Ecto.Query.dynamic(type(^arg1, ^type)) @@ -783,11 +797,11 @@ defmodule AshPostgres.Expr do end defp do_dynamic_expr( - _query, - %Ref{attribute: %Ash.Resource.Attribute{name: name}} = ref, + query, + %Ref{attribute: %Ash.Resource.Attribute{name: name, type: attr_type}} = ref, bindings, _embedded?, - _type + expr_type ) do ref_binding = ref_binding(ref, bindings) @@ -795,7 +809,14 @@ defmodule AshPostgres.Expr do raise "Error while building reference: #{inspect(ref)}" end - Ecto.Query.dynamic(field(as(^ref_binding), ^name)) + case AshPostgres.Types.parameterized_type(attr_type || expr_type, []) do + nil -> + Ecto.Query.dynamic(field(as(^ref_binding), ^name)) + + type -> + validate_type!(query, type, ref) + Ecto.Query.dynamic(type(field(as(^ref_binding), ^name), ^type)) + end end defp do_dynamic_expr(_query, other, _bindings, true, _type) do @@ -806,9 +827,10 @@ defmodule AshPostgres.Expr do end end - defp do_dynamic_expr(_query, value, _bindings, false, {:in, type}) when is_list(value) do + defp do_dynamic_expr(query, value, _bindings, false, {:in, type}) when is_list(value) do value = maybe_sanitize_list(value) + validate_type!(query, type, value) Ecto.Query.dynamic(type(^value, ^{:array, type})) end @@ -823,11 +845,28 @@ defmodule AshPostgres.Expr do Ecto.Query.dynamic(^value) end - defp do_dynamic_expr(_query, value, _bindings, false, type) do + defp do_dynamic_expr(query, value, _bindings, false, type) do value = maybe_sanitize_list(value) + validate_type!(query, type, value) Ecto.Query.dynamic(type(^value, ^type)) end + defp validate_type!(query, type, context) do + case type do + {:parameterized, Ash.Type.CiStringWrapper.EctoType, _} -> + require_extension!(query, "citext", context) + + :ci_string -> + require_extension!(query, "citext", context) + + :citext -> + require_extension!(query, "citext", context) + + _ -> + :ok + end + end + defp maybe_sanitize_list(value) do if is_list(value) do Enum.map(value, fn value -> @@ -871,7 +910,7 @@ defmodule AshPostgres.Expr do defp do_get_path( query, - %GetPath{arguments: [left, right], embedded?: pred_embedded?}, + %GetPath{arguments: [left, right], embedded?: pred_embedded?} = get_expr, bindings, embedded?, type \\ nil @@ -898,6 +937,7 @@ defmodule AshPostgres.Expr do if type do # If we know a type here we use it, since we're pulling out text + validate_type!(query, type, get_expr) Ecto.Query.dynamic(type(^expr, ^type)) else expr @@ -950,6 +990,17 @@ defmodule AshPostgres.Expr do end end + defp require_extension!(query, extension, context) do + repo = AshPostgres.DataLayer.Info.repo(query.__ash_bindings__.resource) + + unless extension in repo.installed_extensions() do + raise Ash.Error.Query.InvalidExpression, + expression: context, + message: + "The #{extension} extension needs to be installed before #{inspect(context)} can be used. Please add \"#{extension}\" to the list of installed_extensions in #{inspect(repo)}." + end + end + defp float_type?({:parameterized, type, params}) when is_atom(type) do type.type(params) in [:float, :decimal] end diff --git a/mix.exs b/mix.exs index 509701a..a32eb76 100644 --- a/mix.exs +++ b/mix.exs @@ -138,7 +138,7 @@ defmodule AshPostgres.MixProject do {:ecto, "~> 3.8"}, {:jason, "~> 1.0"}, {:postgrex, ">= 0.0.0"}, - {:ash, ash_version("~> 2.0.0-rc.7")}, + {:ash, ash_version("~> 2.0.0-rc.9")}, {:git_ops, "~> 2.4.5", only: :dev}, {:ex_doc, "~> 0.22", only: :dev, runtime: false}, {:ex_check, "~> 0.14", only: :dev}, diff --git a/mix.lock b/mix.lock index d2cae39..3568286 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "ash": {:hex, :ash, "2.0.0-rc.7", "3e885022e92e8ed18c9b59225bd4e873d43fe86bf8a6117d0e3e96173aaf6071", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:spark, "~> 0.1 and >= 0.1.9", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ec31d9123a2949f131fb072594920ff971eccf88a0935e3957362424b943e97f"}, + "ash": {:hex, :ash, "2.0.0-rc.9", "c285f783cae15e81fd16c18c0ce7198b060b728f78ea16dc27ae2a642cd6356b", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:spark, "~> 0.1 and >= 0.1.9", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0d6ffc2678193e4f621244a4d81f886bf4acac17e4b7f53c4751025f52ec7287"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"}, @@ -35,7 +35,7 @@ "postgrex": {:hex, :postgrex, "0.16.4", "26d998467b4a22252285e728a29d341e08403d084e44674784975bb1cd00d2cb", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "3234d1a70cb7b1e0c95d2e242785ec2a7a94a092bbcef4472320b950cfd64c5f"}, "sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"}, "sourceror": {:hex, :sourceror, "0.11.2", "549ce48be666421ac60cfb7f59c8752e0d393baa0b14d06271d3f6a8c1b027ab", [:mix], [], "hexpm", "9ab659118896a36be6eec68ff7b0674cba372fc8e210b1e9dc8cf2b55bb70dfb"}, - "spark": {:hex, :spark, "0.1.21", "2a8929f2c894d159a0b079f932944da63fae601538806567c444b45b00b17322", [:mix], [{:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "ca4bd4c2578018bb647ce7e675b592c729aba25a35c59454eb7dd0e2e2a4cc0f"}, + "spark": {:hex, :spark, "0.1.25", "7890f76fd3f32fa588df1c69ae249855b8439d218a7877434a4841aa5d16b41a", [:mix], [{:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "dcdf52b0bc9a1f29ba571547f6d6ad8eecd8458040f896a82f050986d1ae66ac"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, diff --git a/test/filter_test.exs b/test/filter_test.exs index 5ceabc8..353ecf0 100644 --- a/test/filter_test.exs +++ b/test/filter_test.exs @@ -18,6 +18,24 @@ defmodule AshPostgres.FilterTest do end end + describe "citext validation" do + setup do + on_exit(fn -> + Application.delete_env(:ash_postgres, :no_extensions) + end) + end + + test "it raises if you try to use ci_string while ci_text is not installed" do + Application.put_env(:ash_postgres, :no_extensions, ["citext"]) + + assert_raise Ash.Error.Query.InvalidExpression, fn -> + Post + |> Ash.Query.filter(category == "blah") + |> Api.read!() + end + end + end + describe "with a simple filter applied" do test "with no data" do results = diff --git a/test/support/test_repo.ex b/test/support/test_repo.ex index 6220e85..3ab83cd 100644 --- a/test/support/test_repo.ex +++ b/test/support/test_repo.ex @@ -4,7 +4,8 @@ defmodule AshPostgres.TestRepo do otp_app: :ash_postgres def installed_extensions do - ["ash-functions", "uuid-ossp", "pg_trgm", "citext"] + ["ash-functions", "uuid-ossp", "pg_trgm", "citext"] -- + Application.get_env(:ash_postgres, :no_extensions, []) end def all_tenants do