mirror of
https://github.com/ash-project/ash_sqlite.git
synced 2024-09-19 12:52:50 +12:00
improvement: various improvements to data layer, remove explicit distinct features for now
This commit is contained in:
parent
0f854ebd10
commit
16e6f5bf11
11 changed files with 135 additions and 548 deletions
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -2,6 +2,6 @@
|
|||
"cSpell.words": [
|
||||
"citext",
|
||||
"mapset",
|
||||
"strpos"
|
||||
"instr"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
62
lib/expr.ex
62
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,
|
||||
|
|
48
lib/join.ex
48
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(
|
||||
|
|
2
mix.exs
2
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")},
|
||||
|
|
4
mix.lock
4
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"},
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in a new issue