improvement: various improvements to data layer, remove explicit distinct features for now

This commit is contained in:
Zach Daniel 2023-10-05 15:05:03 -04:00
parent 0f854ebd10
commit 16e6f5bf11
11 changed files with 135 additions and 548 deletions

View file

@ -2,6 +2,6 @@
"cSpell.words": [
"citext",
"mapset",
"strpos"
"instr"
]
}

View file

@ -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,22 +447,6 @@ 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)})
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]}
%{
@ -470,7 +454,6 @@ defmodule AshSqlite.DataLayer do
| windows: Keyword.delete(query.windows, :order),
order_bys: [order_by]
}
end
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,6 +716,7 @@ defmodule AshSqlite.DataLayer do
context,
resource
) do
names =
fields
|> String.split(", ")
|> Enum.map(fn field ->
@ -739,9 +727,16 @@ defmodule AshSqlite.DataLayer do
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)

View file

@ -206,15 +206,14 @@ 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((",
raw: "(instr((",
expr: left,
raw: "::citext), (",
raw: " COLLATE NOCASE), (",
expr: right,
raw: ")) > 0)"
]
@ -223,24 +222,6 @@ defmodule AshSqlite.Expr do
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
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,

View file

@ -223,19 +223,25 @@ defmodule AshSqlite.Join do
bindings,
is_subquery?
) do
filter =
resource
|> Ash.Filter.parse!(
relationship.filter,
ash_query.calculations,
Map.update(
ash_query.context,
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,
%{},
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
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
end
defp join_relationship(

View file

@ -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")},

View file

@ -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"},

View file

@ -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

View file

@ -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

View file

@ -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
)

View file

@ -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

View file

@ -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()