From 16e6f5bf11fc6453f3ee73866edecb5decb2e942 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Thu, 5 Oct 2023 15:05:03 -0400 Subject: [PATCH] improvement: various improvements to data layer, remove explicit distinct features for now --- .vscode/settings.json | 4 +- lib/data_layer.ex | 323 ++++-------------- lib/expr.ex | 62 ++-- lib/join.ex | 48 ++- mix.exs | 2 +- mix.lock | 4 +- test/distinct_test.exs | 171 ---------- .../comments_containing_title.ex | 6 +- test/support/resources/post.ex | 5 +- test/support/types/point.ex | 34 -- test/type_test.exs | 24 -- 11 files changed, 135 insertions(+), 548 deletions(-) delete mode 100644 test/distinct_test.exs delete mode 100644 test/support/types/point.ex diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f9c00f..886e853 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,6 @@ "cSpell.words": [ "citext", "mapset", - "strpos" + "instr" ] -} \ No newline at end of file +} diff --git a/lib/data_layer.ex b/lib/data_layer.ex index dc48e69..2910feb 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -283,7 +283,7 @@ defmodule AshSqlite.DataLayer do } alias Ash.Filter - alias Ash.Query.{BooleanExpression, Not, Ref} + alias Ash.Query.{BooleanExpression, Not} @behaviour Ash.DataLayer @@ -383,8 +383,8 @@ defmodule AshSqlite.DataLayer do def can?(_, :nested_expressions), do: true def can?(_, {:query_aggregate, :count}), do: false def can?(_, :sort), do: true - def can?(_, :distinct_sort), do: true - def can?(_, :distinct), do: true + def can?(_, :distinct_sort), do: false + def can?(_, :distinct), do: false def can?(_, {:sort, _}), do: true def can?(_, _), do: false @@ -447,30 +447,13 @@ defmodule AshSqlite.DataLayer do {:ok, query} -> query = if query.__ash_bindings__[:__order__?] && query.windows[:order] do - if query.distinct do - query_with_order = - from(row in query, select_merge: %{__order__: over(row_number(), :order)}) + order_by = %{query.windows[:order] | expr: query.windows[:order].expr[:order_by]} - query_without_limit_and_offset = - query_with_order - |> Ecto.Query.exclude(:limit) - |> Ecto.Query.exclude(:offset) - - from(row in subquery(query_without_limit_and_offset), - select: row, - order_by: row.__order__ - ) - |> Map.put(:limit, query.limit) - |> Map.put(:offset, query.offset) - else - order_by = %{query.windows[:order] | expr: query.windows[:order].expr[:order_by]} - - %{ - query - | windows: Keyword.delete(query.windows, :order), - order_bys: [order_by] - } - end + %{ + query + | windows: Keyword.delete(query.windows, :order), + order_bys: [order_by] + } else %{query | windows: Keyword.delete(query.windows, :order)} end @@ -478,7 +461,11 @@ defmodule AshSqlite.DataLayer do if AshSqlite.DataLayer.Info.polymorphic?(resource) && no_table?(query) do raise_table_error!(resource, :read) else - {:ok, dynamic_repo(resource, query).all(query, repo_opts(nil, nil, resource))} + primary_key = Ash.Resource.Info.primary_key(resource) + + {:ok, + dynamic_repo(resource, query).all(query, repo_opts(nil, nil, resource)) + |> Enum.uniq_by(&Map.take(&1, primary_key))} end end rescue @@ -729,19 +716,27 @@ defmodule AshSqlite.DataLayer do context, resource ) do - fields - |> String.split(", ") - |> Enum.map(fn field -> - field |> String.split(".", trim: true) |> Enum.drop(1) |> Enum.at(0) - end) - |> Enum.map(fn field -> - Ash.Resource.Info.attribute(resource, field) - end) - |> Enum.reject(&is_nil/1) - |> Enum.map(fn %{name: name} -> + names = + fields + |> String.split(", ") + |> Enum.map(fn field -> + field |> String.split(".", trim: true) |> Enum.drop(1) |> Enum.at(0) + end) + |> Enum.map(fn field -> + Ash.Resource.Info.attribute(resource, field) + end) + |> Enum.reject(&is_nil/1) + |> Enum.map(fn %{name: name} -> + name + end) + + message = find_constraint_message(resource, names) + + names + |> Enum.map(fn name -> Ash.Error.Changes.InvalidAttribute.exception( field: name, - message: "has already been taken" + message: message ) end) |> handle_raised_error( @@ -755,6 +750,40 @@ defmodule AshSqlite.DataLayer do {:error, Ash.Error.to_ash_error(error, stacktrace)} end + defp find_constraint_message(resource, names) do + find_custom_index_message(resource, names) || find_identity_message(resource, names) || + "has already been taken" + end + + defp find_custom_index_message(resource, names) do + resource + |> AshSqlite.DataLayer.Info.custom_indexes() + |> Enum.find(fn %{fields: fields} -> + fields |> Enum.map(&to_string/1) |> Enum.sort() == + names |> Enum.map(&to_string/1) |> Enum.sort() + end) + |> case do + %{message: message} when is_binary(message) -> message + _ -> nil + end + end + + defp find_identity_message(resource, names) do + resource + |> Ash.Resource.Info.identities() + |> Enum.find(fn %{keys: fields} -> + fields |> Enum.map(&to_string/1) |> Enum.sort() == + names |> Enum.map(&to_string/1) |> Enum.sort() + end) + |> case do + %{message: message} when is_binary(message) -> + message + + _ -> + nil + end + end + defp set_table(record, changeset, operation, table_error?) do if AshSqlite.DataLayer.Info.polymorphic?(record.__struct__) do table = @@ -1255,104 +1284,6 @@ defmodule AshSqlite.DataLayer do )} end - @impl true - def distinct_sort(query, sort, _) when sort in [nil, []] do - {:ok, query} - end - - def distinct_sort(query, sort, _) do - {:ok, Map.update!(query, :__ash_bindings__, &Map.put(&1, :distinct_sort, sort))} - end - - # If the order by does not match the initial sort clause, then we use a subquery - # to limit to only distinct rows. This may not perform that well, so we may need - # to come up with alternatives here. - @impl true - def distinct(query, empty, resource) when empty in [nil, []] do - query |> apply_sort(query.__ash_bindings__[:sort], resource) - end - - def distinct(query, distinct_on, resource) do - case get_distinct_statement(query, distinct_on) do - {:ok, distinct_statement} -> - %{query | distinct: distinct_statement} - |> apply_sort(query.__ash_bindings__[:sort], resource) - - {:error, distinct_statement} -> - query - |> Ecto.Query.exclude(:order_by) - |> default_bindings(resource) - |> Map.put(:distinct, distinct_statement) - |> apply_sort( - query.__ash_bindings__[:distinct_sort] || query.__ash_bindings__[:sort], - resource, - true - ) - |> case do - {:ok, distinct_query} -> - on = - Enum.reduce(Ash.Resource.Info.primary_key(resource), nil, fn key, dynamic -> - if dynamic do - Ecto.Query.dynamic( - [row, distinct], - ^dynamic and field(row, ^key) == field(distinct, ^key) - ) - else - Ecto.Query.dynamic([row, distinct], field(row, ^key) == field(distinct, ^key)) - end - end) - - joined_query_source = - Enum.reduce( - [ - :join, - :order_by, - :group_by, - :having, - :distinct, - :select, - :combinations, - :with_ctes, - :limit, - :offset, - :preload, - :update, - :where - ], - query, - &Ecto.Query.exclude(&2, &1) - ) - - joined_query = - from(row in joined_query_source, - join: distinct in subquery(distinct_query), - on: ^on - ) - - from([row, distinct] in joined_query, - select: distinct - ) - |> default_bindings(resource) - |> apply_sort(query.__ash_bindings__[:sort], resource) - |> case do - {:ok, joined_query} -> - {:ok, - Map.update!( - joined_query, - :__ash_bindings__, - &Map.put(&1, :__order__?, query.__ash_bindings__[:__order__?] || false) - )} - - {:error, error} -> - {:error, error} - end - - {:error, error} -> - {:error, error} - end - end - end - defp apply_sort(query, sort, resource, directly? \\ false) defp apply_sort(query, sort, _resource, _) when sort in [nil, []] do @@ -1374,125 +1305,15 @@ defmodule AshSqlite.DataLayer do end end + @doc false + def unwrap_one([thing]), do: thing + def unwrap_one([]), do: nil + def unwrap_one(other), do: other + defp set_sort_applied(query) do Map.update!(query, :__ash_bindings__, &Map.put(&1, :sort_applied?, true)) end - defp get_distinct_statement(query, distinct_on) do - has_distinct_sort? = match?(%{__ash_bindings__: %{distinct_sort: _}}, query) - - if has_distinct_sort? do - {:error, default_distinct_statement(query, distinct_on)} - else - sort = query.__ash_bindings__[:sort] || [] - - distinct = - query.distinct || - %Ecto.Query.QueryExpr{ - expr: [], - params: [] - } - - if sort == [] do - {:ok, default_distinct_statement(query, distinct_on)} - else - distinct_on - |> Enum.reduce_while({sort, [], [], Enum.count(distinct.params)}, fn - _, {[], _distinct_statement, _, _count} -> - {:halt, :error} - - distinct_on, {[order_by | rest_order_by], distinct_statement, params, count} -> - case order_by do - {^distinct_on, order} -> - {distinct_expr, params, count} = - distinct_on_expr(query, distinct_on, params, count) - - {:cont, - {rest_order_by, [{order, distinct_expr} | distinct_statement], params, count}} - - _ -> - {:halt, :error} - end - end) - |> case do - :error -> - {:error, default_distinct_statement(query, distinct_on)} - - {_, result, params, _} -> - {:ok, - %{ - distinct - | expr: distinct.expr ++ Enum.reverse(result), - params: distinct.params ++ Enum.reverse(params) - }} - end - end - end - end - - defp default_distinct_statement(query, distinct_on) do - distinct = - query.distinct || - %Ecto.Query.QueryExpr{ - expr: [] - } - - {expr, params, _} = - Enum.reduce(distinct_on, {[], [], Enum.count(distinct.params)}, fn - {distinct_on_field, order}, {expr, params, count} -> - {distinct_expr, params, count} = - distinct_on_expr(query, distinct_on_field, params, count) - - {[{order, distinct_expr} | expr], params, count} - - distinct_on_field, {expr, params, count} -> - {distinct_expr, params, count} = - distinct_on_expr(query, distinct_on_field, params, count) - - {[{:asc, distinct_expr} | expr], params, count} - end) - - %{ - distinct - | expr: distinct.expr ++ Enum.reverse(expr), - params: distinct.params ++ Enum.reverse(params) - } - end - - defp distinct_on_expr(query, field, params, count) do - resource = query.__ash_bindings__.resource - - ref = - case field do - %Ash.Query.Calculation{} = calc -> - %Ref{attribute: calc, relationship_path: [], resource: resource} - - field -> - %Ref{ - attribute: Ash.Resource.Info.field(resource, field), - relationship_path: [], - resource: resource - } - end - - dynamic = AshSqlite.Expr.dynamic_expr(query, ref, query.__ash_bindings__) - - result = - Ecto.Query.Builder.Dynamic.partially_expand( - :distinct, - query, - dynamic, - params, - count - ) - - expr = elem(result, 0) - new_params = elem(result, 1) - new_count = result |> Tuple.to_list() |> List.last() - - {expr, new_params, new_count} - end - @impl true def filter(query, filter, resource, opts \\ []) do query = default_bindings(query, resource) diff --git a/lib/expr.ex b/lib/expr.ex index 4b46f34..027a60f 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -206,41 +206,22 @@ defmodule AshSqlite.Expr do embedded?, type ) do - if "citext" in AshSqlite.DataLayer.Info.repo(query.__ash_bindings__.resource).installed_extensions() do - do_dynamic_expr( - query, - %Fragment{ - embedded?: pred_embedded?, - arguments: [ - raw: "(strpos((", - expr: left, - raw: "::citext), (", - expr: right, - raw: ")) > 0)" - ] - }, - bindings, - embedded?, - type - ) - else - do_dynamic_expr( - query, - %Fragment{ - embedded?: pred_embedded?, - arguments: [ - raw: "(strpos(lower(", - expr: left, - raw: "), lower(", - expr: right, - raw: ")) > 0)" - ] - }, - bindings, - embedded?, - type - ) - end + do_dynamic_expr( + query, + %Fragment{ + embedded?: pred_embedded?, + arguments: [ + raw: "(instr((", + expr: left, + raw: " COLLATE NOCASE), (", + expr: right, + raw: ")) > 0)" + ] + }, + bindings, + embedded?, + type + ) end defp do_dynamic_expr( @@ -255,7 +236,7 @@ defmodule AshSqlite.Expr do %Fragment{ embedded?: pred_embedded?, arguments: [ - raw: "(strpos((", + raw: "(instr((", expr: left, raw: "), (", expr: right, @@ -1107,6 +1088,7 @@ defmodule AshSqlite.Expr do if is_list(other) do list_expr(query, other, bindings, true, type) else + IO.inspect(other, structs: false) raise "Unsupported expression in AshSqlite query: #{inspect(other)}" end else @@ -1246,10 +1228,10 @@ defmodule AshSqlite.Expr do path_frags = path |> Enum.flat_map(fn item -> - [expr: item, raw: "::text,"] + [expr: item, raw: ","] end) |> :lists.droplast() - |> Enum.concat(raw: "::text)") + |> Enum.concat(raw: ")") expr = do_dynamic_expr( @@ -1258,9 +1240,9 @@ defmodule AshSqlite.Expr do embedded?: pred_embedded?, arguments: [ - raw: "jsonb_extract_path_text(", + raw: "jsonb_extract_path(", expr: left, - raw: "::jsonb," + raw: "," ] ++ path_frags }, bindings, diff --git a/lib/join.ex b/lib/join.ex index 95c8b3c..365c0ca 100644 --- a/lib/join.ex +++ b/lib/join.ex @@ -223,19 +223,25 @@ defmodule AshSqlite.Join do bindings, is_subquery? ) do + context = + ash_query.context + |> Map.update( + :parent_stack, + [relationship.source], + &[&1 | relationship.source] + ) + |> Map.put(:resource, relationship.destination) + filter = resource |> Ash.Filter.parse!( relationship.filter, - ash_query.calculations, - Map.update( - ash_query.context, - :parent_stack, - [relationship.source], - &[&1 | relationship.source] - ) + %{}, + context ) + {:ok, filter} = Ash.Filter.hydrate_refs(filter, context) + base_bindings = bindings || query.__ash_bindings__ parent_binding = @@ -405,16 +411,24 @@ defmodule AshSqlite.Join do def get_binding(_, _, _, _), do: nil - defp add_distinct(relationship, _join_type, joined_query) do - if !joined_query.__ash_bindings__.in_group? && - (relationship.cardinality == :many || Map.get(relationship, :from_many?)) && - !joined_query.distinct do - from(row in joined_query, - distinct: ^Ash.Resource.Info.primary_key(joined_query.__ash_bindings__.resource) - ) - else - joined_query - end + defp add_distinct(_relationship, _join_type, joined_query) do + # We can't do the same distincting that we do in ash_postgres + # This means that all filters that reference `has_many` relationships need + # to be rewritten to use `exists`, which will allow us to not need to do any distincting. + # in fact, we probably want to do that in `ash_postgres` automatically too? + # if !joined_query.__ash_bindings__.in_group? && + # (relationship.cardinality == :many || Map.get(relationship, :from_many?)) && + # !joined_query.distinct do + # from(row in joined_query, + # distinct: + # ^AshSqlite.DataLayer.unwrap_one( + # Ash.Resource.Info.primary_key(joined_query.__ash_bindings__.resource) + # ) + # ) + # |> IO.inspect() + # else + joined_query + # end end defp join_relationship( diff --git a/mix.exs b/mix.exs index 89a3fb2..673d2a4 100644 --- a/mix.exs +++ b/mix.exs @@ -172,7 +172,7 @@ defmodule AshSqlite.MixProject do defp deps do [ {:ecto_sql, "~> 3.9"}, - {:ecto_sqlite3, "~> 0.11"}, + {:ecto_sqlite3, path: "../ecto_sqlite3", override: true}, {:ecto, "~> 3.9"}, {:jason, "~> 1.0"}, {:ash, ash_version("~> 2.14 and >= 2.14.18")}, diff --git a/mix.lock b/mix.lock index 4459827..6d41d39 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "ash": {:hex, :ash, "2.14.18", "ac2fd2f274f4989d3c71de3df9a603941bc47ac6c8d27006df78f78844114969", [: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", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, ">= 1.1.20 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ec44ad258eb71a2dd5210f67bd882698ea112f6dad79505b156594be06e320e5"}, + "ash": {:hex, :ash, "2.15.8", "e1de02bfb08c13b24f162c0e20e7e2be2019d9df92c71c76b62178b6ab50baab", [: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", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, ">= 1.1.20 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.6", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e85ddb64ab9b5390cea44571f9980cadba7081a0989fe77de948182770047d1"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.8", "933a5f4da3b19ee56539a076076ce4d7716d64efc8db46fd066996a7e46e2bfd", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "176bdf4366956e456bf761b54ad70bc4103d0269ca9558fd7cee93d1b3f116db"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, @@ -36,7 +36,7 @@ "picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"}, "sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"}, "sourceror": {:hex, :sourceror, "0.14.0", "b6b8552d0240400d66b6f107c1bab7ac1726e998efc797f178b7b517e928e314", [:mix], [], "hexpm", "809c71270ad48092d40bbe251a133e49ae229433ce103f762a2373b7a10a8d8b"}, - "spark": {:hex, :spark, "1.1.39", "f143b84a5b796bf2d83ec8fb4793ee9e66e67510c40d785f9a67050bb88e7677", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "d71bc26014c7e7abcdcf553f4cf7c5a5ff96f8365b1e20be3768ce503aafb203"}, + "spark": {:hex, :spark, "1.1.41", "c34c7bec8b91f8af05690b5500b5287a319c10887a2e1db6fa5e203289ba62c8", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "908548f3cbf84d402869e1caf7b5a78492e7d171fe492affc02748f2a51746ff"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, diff --git a/test/distinct_test.exs b/test/distinct_test.exs deleted file mode 100644 index 408ce02..0000000 --- a/test/distinct_test.exs +++ /dev/null @@ -1,171 +0,0 @@ -defmodule AshSqlite.DistinctTest do - @moduledoc false - use AshSqlite.RepoCase, async: false - alias AshSqlite.Test.{Api, Post} - - require Ash.Query - - setup do - Post - |> Ash.Changeset.new(%{title: "title", score: 1}) - |> Api.create!() - - Post - |> Ash.Changeset.new(%{title: "title", score: 1}) - |> Api.create!() - - Post - |> Ash.Changeset.new(%{title: "foo", score: 2}) - |> Api.create!() - - Post - |> Ash.Changeset.new(%{title: "foo", score: 2}) - |> Api.create!() - - :ok - end - - test "records returned are distinct on the provided field" do - results = - Post - |> Ash.Query.distinct(:title) - |> Ash.Query.sort(:title) - |> Api.read!() - - assert [%{title: "foo"}, %{title: "title"}] = results - end - - test "distinct pairs well with sort" do - results = - Post - |> Ash.Query.distinct(:title) - |> Ash.Query.sort(title: :desc) - |> Api.read!() - - assert [%{title: "title"}, %{title: "foo"}] = results - end - - test "distinct pairs well with sort that does not match the distinct" do - results = - Post - |> Ash.Query.distinct(:title) - |> Ash.Query.sort(id: :desc) - |> Ash.Query.limit(3) - |> Api.read!() - - assert [_, _] = results - end - - test "distinct pairs well with sort that does not match the distinct using a limit" do - results = - Post - |> Ash.Query.distinct(:title) - |> Ash.Query.sort(id: :desc) - |> Ash.Query.limit(3) - |> Api.read!() - - assert [_, _] = results - end - - test "distinct pairs well with sort that does not match the distinct using a limit #2" do - results = - Post - |> Ash.Query.distinct(:title) - |> Ash.Query.sort(id: :desc) - |> Ash.Query.limit(1) - |> Api.read!() - - assert [_] = results - end - - test "distinct can use calculations sort that does not match the distinct using a limit #2" do - results = - Post - |> Ash.Query.distinct(:negative_score) - |> Ash.Query.sort(:negative_score) - |> Ash.Query.load(:negative_score) - |> Api.read!() - - assert [ - %{title: "foo", negative_score: -2}, - %{title: "title", negative_score: -1} - ] = results - - results = - Post - |> Ash.Query.distinct(:negative_score) - |> Ash.Query.sort(negative_score: :desc) - |> Ash.Query.load(:negative_score) - |> Api.read!() - - assert [ - %{title: "title", negative_score: -1}, - %{title: "foo", negative_score: -2} - ] = results - - results = - Post - |> Ash.Query.distinct(:negative_score) - |> Ash.Query.sort(:title) - |> Ash.Query.load(:negative_score) - |> Api.read!() - - assert [ - %{title: "foo", negative_score: -2}, - %{title: "title", negative_score: -1} - ] = results - end - - test "distinct, join filters and sort can be combined" do - Post - |> Ash.Changeset.new(%{title: "a", score: 2}) - |> Api.create!() - - Post - |> Ash.Changeset.new(%{title: "a", score: 1}) - |> Api.create!() - - assert [] = - Post - |> Ash.Query.distinct(:negative_score) - |> Ash.Query.filter(author.first_name == "a") - |> Ash.Query.sort(:negative_score) - |> Api.read!() - end - - test "distinct sort is applied" do - Post - |> Ash.Changeset.new(%{title: "a", score: 2}) - |> Api.create!() - - Post - |> Ash.Changeset.new(%{title: "a", score: 1}) - |> Api.create!() - - results = - Post - |> Ash.Query.distinct(:negative_score) - |> Ash.Query.distinct_sort(:title) - |> Ash.Query.sort(:negative_score) - |> Ash.Query.load(:negative_score) - |> Api.read!() - - assert [ - %{title: "a", negative_score: -2}, - %{title: "a", negative_score: -1} - ] = results - - results = - Post - |> Ash.Query.distinct(:negative_score) - |> Ash.Query.distinct_sort(title: :desc) - |> Ash.Query.sort(:negative_score) - |> Ash.Query.load(:negative_score) - |> Api.read!() - - assert [ - %{title: "foo", negative_score: -2}, - %{title: "title", negative_score: -1} - ] = results - end -end diff --git a/test/support/relationships/comments_containing_title.ex b/test/support/relationships/comments_containing_title.ex index e8439be..dcac7ec 100644 --- a/test/support/relationships/comments_containing_title.ex +++ b/test/support/relationships/comments_containing_title.ex @@ -23,7 +23,7 @@ defmodule AshSqlite.Test.Post.CommentsContainingTitle do join: dest in ^destination_query, as: ^as_binding, on: dest.post_id == as(^current_binding).id, - on: fragment("strpos(?, ?) > 0", dest.title, as(^current_binding).title) + on: fragment("instr(?, ?) > 0", dest.title, as(^current_binding).title) )} end @@ -33,7 +33,7 @@ defmodule AshSqlite.Test.Post.CommentsContainingTitle do left_join: dest in ^destination_query, as: ^as_binding, on: dest.post_id == as(^current_binding).id, - on: fragment("strpos(?, ?) > 0", dest.title, as(^current_binding).title) + on: fragment("instr(?, ?) > 0", dest.title, as(^current_binding).title) )} end @@ -42,7 +42,7 @@ defmodule AshSqlite.Test.Post.CommentsContainingTitle do Ecto.Query.from(_ in destination_query, where: parent_as(^current_binding).id == as(^as_binding).post_id, where: - fragment("strpos(?, ?) > 0", as(^as_binding).title, parent_as(^current_binding).title) + fragment("instr(?, ?) > 0", as(^as_binding).title, parent_as(^current_binding).title) )} end end diff --git a/test/support/resources/post.ex b/test/support/resources/post.ex index 2e76636..0051fce 100644 --- a/test/support/resources/post.ex +++ b/test/support/resources/post.ex @@ -76,7 +76,6 @@ defmodule AshSqlite.Test.Post do attribute(:status, AshSqlite.Test.Types.Status) attribute(:status_enum, AshSqlite.Test.Types.StatusEnum) attribute(:status_enum_no_cast, AshSqlite.Test.Types.StatusEnumNoCast, source: :status_enum) - attribute(:point, AshSqlite.Test.Point) attribute(:stuff, :map) attribute(:uniq_one, :string) attribute(:uniq_two, :string) @@ -163,7 +162,7 @@ defmodule AshSqlite.Test.Post do AshSqlite.Test.Money, expr( fragment(""" - '{"amount":100, "currency": "usd"}'::json + '{"amount":100, "currency": "usd"}' """) ) ) @@ -174,7 +173,7 @@ defmodule AshSqlite.Test.Post do expr( # This is written in a silly way on purpose, to test a regression if( - fragment("(? <= (? - '1 month'::interval))", now(), created_at), + fragment("(? <= (DATE(? - '+1 month')))", now(), created_at), true, false ) diff --git a/test/support/types/point.ex b/test/support/types/point.ex deleted file mode 100644 index 50baa89..0000000 --- a/test/support/types/point.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule AshSqlite.Test.Point do - @moduledoc false - use Ash.Type - - def storage_type(_), do: {:array, :float} - - def cast_input(nil, _), do: {:ok, nil} - - def cast_input({a, b, c}, _) when is_float(a) and is_float(b) and is_float(c) do - {:ok, {a, b, c}} - end - - def cast_input(_, _), do: :error - - def cast_stored(nil, _), do: {:ok, nil} - - def cast_stored([a, b, c], _) when is_float(a) and is_float(b) and is_float(c) do - {:ok, {a, b, c}} - end - - def cast_stored(_, _) do - :error - end - - def dump_to_native(nil, _), do: {:ok, nil} - - def dump_to_native({a, b, c}, _) when is_float(a) and is_float(b) and is_float(c) do - {:ok, [a, b, c]} - end - - def dump_to_native(_, _) do - :error - end -end diff --git a/test/type_test.exs b/test/type_test.exs index 0263835..5b41e4d 100644 --- a/test/type_test.exs +++ b/test/type_test.exs @@ -4,30 +4,6 @@ defmodule AshSqlite.Test.TypeTest do require Ash.Query - test "complex custom types can be used" do - post = - Post - |> Ash.Changeset.new(%{title: "title", point: {1.0, 2.0, 3.0}}) - |> Api.create!() - - assert post.point == {1.0, 2.0, 3.0} - end - - test "complex custom types can be accessed with fragments" do - Post - |> Ash.Changeset.new(%{title: "title", point: {1.0, 2.0, 3.0}}) - |> Api.create!() - - Post - |> Ash.Changeset.new(%{title: "title", point: {2.0, 1.0, 3.0}}) - |> Api.create!() - - assert [%{point: {2.0, 1.0, 3.0}}] = - Post - |> Ash.Query.filter(fragment("(?)[1] > (?)[2]", point, point)) - |> Api.read!() - end - test "uuids can be used as strings in fragments" do uuid = Ash.UUID.generate()