mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-19 13:03:14 +12:00
improvement: support for error/2
expression
This commit is contained in:
parent
54908395eb
commit
407a7163ed
9 changed files with 476 additions and 228 deletions
|
@ -645,7 +645,11 @@ defmodule AshPostgres.DataLayer do
|
|||
if AshPostgres.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))}
|
||||
repo = dynamic_repo(resource, query)
|
||||
|
||||
with_savepoint(repo, fn ->
|
||||
{:ok, repo.all(query, repo_opts(nil, nil, resource))}
|
||||
end)
|
||||
end
|
||||
end
|
||||
rescue
|
||||
|
@ -1195,100 +1199,138 @@ defmodule AshPostgres.DataLayer do
|
|||
|
||||
changesets = Enum.to_list(stream)
|
||||
|
||||
opts =
|
||||
if options[:upsert?] do
|
||||
# Ash groups changesets by atomics before dispatching them to the data layer
|
||||
# this means that all changesets have the same atomics
|
||||
%{atomics: atomics, filters: filters} = Enum.at(changesets, 0)
|
||||
|
||||
query = from(row in resource, as: ^0)
|
||||
|
||||
query =
|
||||
query
|
||||
|> default_bindings(resource)
|
||||
|
||||
upsert_set =
|
||||
upsert_set(resource, changesets, options)
|
||||
|
||||
on_conflict =
|
||||
case query_with_atomics(
|
||||
resource,
|
||||
query,
|
||||
filters,
|
||||
atomics,
|
||||
%{},
|
||||
upsert_set
|
||||
) do
|
||||
:empty ->
|
||||
{:replace, options[:upsert_keys] || Ash.Resource.Info.primary_key(resource)}
|
||||
|
||||
{:ok, query} ->
|
||||
query
|
||||
|
||||
{:error, error} ->
|
||||
raise Ash.Error.to_ash_error(error)
|
||||
end
|
||||
|
||||
opts
|
||||
|> Keyword.put(:on_conflict, on_conflict)
|
||||
|> Keyword.put(
|
||||
:conflict_target,
|
||||
conflict_target(
|
||||
resource,
|
||||
options[:upsert_keys] || Ash.Resource.Info.primary_key(resource)
|
||||
)
|
||||
)
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
ecto_changesets = Enum.map(changesets, & &1.attributes)
|
||||
|
||||
source =
|
||||
if table = Enum.at(changesets, 0).context[:data_layer][:table] do
|
||||
{table, resource}
|
||||
else
|
||||
resource
|
||||
end
|
||||
|
||||
repo = dynamic_repo(resource, Enum.at(changesets, 0))
|
||||
|
||||
source
|
||||
|> repo.insert_all(ecto_changesets, opts)
|
||||
|> case do
|
||||
{_, nil} ->
|
||||
:ok
|
||||
try do
|
||||
opts =
|
||||
if options[:upsert?] do
|
||||
# Ash groups changesets by atomics before dispatching them to the data layer
|
||||
# this means that all changesets have the same atomics
|
||||
%{atomics: atomics, filters: filters} = Enum.at(changesets, 0)
|
||||
|
||||
{_, results} ->
|
||||
if options[:single?] do
|
||||
Enum.each(results, &maybe_create_tenant!(resource, &1))
|
||||
query = from(row in resource, as: ^0)
|
||||
|
||||
{:ok, results}
|
||||
query =
|
||||
query
|
||||
|> default_bindings(resource)
|
||||
|
||||
upsert_set =
|
||||
upsert_set(resource, changesets, options)
|
||||
|
||||
on_conflict =
|
||||
case query_with_atomics(
|
||||
resource,
|
||||
query,
|
||||
filters,
|
||||
atomics,
|
||||
%{},
|
||||
upsert_set
|
||||
) do
|
||||
:empty ->
|
||||
{:replace, options[:upsert_keys] || Ash.Resource.Info.primary_key(resource)}
|
||||
|
||||
{:ok, query} ->
|
||||
query
|
||||
|
||||
{:error, error} ->
|
||||
raise Ash.Error.to_ash_error(error)
|
||||
end
|
||||
|
||||
opts
|
||||
|> Keyword.put(:on_conflict, on_conflict)
|
||||
|> Keyword.put(
|
||||
:conflict_target,
|
||||
conflict_target(
|
||||
resource,
|
||||
options[:upsert_keys] || Ash.Resource.Info.primary_key(resource)
|
||||
)
|
||||
)
|
||||
else
|
||||
{:ok,
|
||||
Stream.zip_with(results, changesets, fn result, changeset ->
|
||||
if !opts[:upsert?] do
|
||||
maybe_create_tenant!(resource, result)
|
||||
end
|
||||
|
||||
Ash.Resource.put_metadata(
|
||||
result,
|
||||
:bulk_create_index,
|
||||
changeset.context.bulk_create.index
|
||||
)
|
||||
end)}
|
||||
opts
|
||||
end
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
changeset = Ash.Changeset.new(resource)
|
||||
|
||||
handle_raised_error(
|
||||
e,
|
||||
__STACKTRACE__,
|
||||
{:bulk_create, ecto_changeset(changeset.data, changeset, :create, false)},
|
||||
resource
|
||||
)
|
||||
ecto_changesets = Enum.map(changesets, & &1.attributes)
|
||||
|
||||
source =
|
||||
if table = Enum.at(changesets, 0).context[:data_layer][:table] do
|
||||
{table, resource}
|
||||
else
|
||||
resource
|
||||
end
|
||||
|
||||
result =
|
||||
with_savepoint(repo, fn ->
|
||||
repo.insert_all(source, ecto_changesets, opts)
|
||||
end)
|
||||
|
||||
case result do
|
||||
{_, nil} ->
|
||||
:ok
|
||||
|
||||
{_, results} ->
|
||||
if options[:single?] do
|
||||
Enum.each(results, &maybe_create_tenant!(resource, &1))
|
||||
|
||||
{:ok, results}
|
||||
else
|
||||
{:ok,
|
||||
Stream.zip_with(results, changesets, fn result, changeset ->
|
||||
if !opts[:upsert?] do
|
||||
maybe_create_tenant!(resource, result)
|
||||
end
|
||||
|
||||
Ash.Resource.put_metadata(
|
||||
result,
|
||||
:bulk_create_index,
|
||||
changeset.context.bulk_create.index
|
||||
)
|
||||
end)}
|
||||
end
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
changeset = Ash.Changeset.new(resource)
|
||||
|
||||
handle_raised_error(
|
||||
e,
|
||||
__STACKTRACE__,
|
||||
{:bulk_create, ecto_changeset(changeset.data, changeset, :create, false)},
|
||||
resource
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp with_savepoint(repo, fun) do
|
||||
if repo.in_transaction?() do
|
||||
savepoint_id = "a" <> (Ash.UUID.generate() |> String.replace("-", "_"))
|
||||
|
||||
repo.query!("SAVEPOINT #{savepoint_id}")
|
||||
|
||||
result =
|
||||
try do
|
||||
{:ok, fun.()}
|
||||
rescue
|
||||
e ->
|
||||
repo.query!("ROLLBACK TO #{savepoint_id}")
|
||||
{:exception, e, __STACKTRACE__}
|
||||
end
|
||||
|
||||
case result do
|
||||
{:exception, e, stacktrace} ->
|
||||
reraise e, stacktrace
|
||||
|
||||
{:ok, result} ->
|
||||
repo.query!("RELEASE #{savepoint_id}")
|
||||
result
|
||||
end
|
||||
else
|
||||
try do
|
||||
fun.()
|
||||
rescue
|
||||
e ->
|
||||
reraise e, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp upsert_set(resource, changesets, options) do
|
||||
|
@ -1583,6 +1625,29 @@ defmodule AshPostgres.DataLayer do
|
|||
end
|
||||
end
|
||||
|
||||
defp handle_raised_error(
|
||||
%Postgrex.Error{
|
||||
postgres: %{
|
||||
code: :raise_exception,
|
||||
message: "\"ash_exception: " <> json,
|
||||
severity: "ERROR"
|
||||
}
|
||||
},
|
||||
_,
|
||||
_,
|
||||
_
|
||||
) do
|
||||
%{"exception" => exception, "input" => input} =
|
||||
json
|
||||
|> String.trim_trailing("\"")
|
||||
|> String.replace("\\\"", "\"")
|
||||
|> Jason.decode!()
|
||||
|
||||
exception = Module.concat([exception])
|
||||
|
||||
{:error, Ash.Error.from_json(exception, input)}
|
||||
end
|
||||
|
||||
defp handle_raised_error(error, stacktrace, _ecto_changeset, _resource) do
|
||||
{:error, Ash.Error.to_ash_error(error, stacktrace)}
|
||||
end
|
||||
|
@ -2020,12 +2085,16 @@ defmodule AshPostgres.DataLayer do
|
|||
repo_opts =
|
||||
Keyword.put(repo_opts, :returning, Keyword.keys(changeset.atomics))
|
||||
|
||||
repo = dynamic_repo(resource, changeset)
|
||||
|
||||
result =
|
||||
dynamic_repo(resource, changeset).update_all(
|
||||
query,
|
||||
[],
|
||||
repo_opts
|
||||
)
|
||||
with_savepoint(repo, fn ->
|
||||
repo.update_all(
|
||||
query,
|
||||
[],
|
||||
repo_opts
|
||||
)
|
||||
end)
|
||||
|
||||
case result do
|
||||
{0, []} ->
|
||||
|
@ -2174,10 +2243,17 @@ defmodule AshPostgres.DataLayer do
|
|||
ecto_changeset = ecto_changeset(record, changeset, :delete)
|
||||
|
||||
try do
|
||||
ecto_changeset
|
||||
|> dynamic_repo(resource, changeset).delete(
|
||||
repo_opts(changeset.timeout, changeset.tenant, changeset.resource)
|
||||
)
|
||||
repo = dynamic_repo(resource, changeset)
|
||||
|
||||
result =
|
||||
with_savepoint(repo, fn ->
|
||||
repo.delete(
|
||||
ecto_changeset,
|
||||
repo_opts(changeset.timeout, changeset.tenant, changeset.resource)
|
||||
)
|
||||
end)
|
||||
|
||||
result
|
||||
|> from_ecto()
|
||||
|> case do
|
||||
{:ok, _record} ->
|
||||
|
|
32
lib/expr.ex
32
lib/expr.ex
|
@ -17,6 +17,7 @@ defmodule AshPostgres.Expr do
|
|||
If,
|
||||
Length,
|
||||
Now,
|
||||
Error,
|
||||
StringJoin,
|
||||
StringSplit,
|
||||
Today,
|
||||
|
@ -1041,6 +1042,37 @@ defmodule AshPostgres.Expr do
|
|||
)
|
||||
end
|
||||
|
||||
defp do_dynamic_expr(
|
||||
query,
|
||||
%Error{arguments: [exception, input]} = value,
|
||||
_bindings,
|
||||
_embedded?,
|
||||
type
|
||||
) do
|
||||
require_ash_functions!(query, "error/2")
|
||||
|
||||
unless Keyword.keyword?(input) || is_map(input) do
|
||||
raise "Input expression to `error` must be a map or keyword list"
|
||||
end
|
||||
|
||||
encoded =
|
||||
"ash_exception: " <> Jason.encode!(%{exception: inspect(exception), input: Map.new(input)})
|
||||
|
||||
if type do
|
||||
# This is a type hint, if we're raising an error, we tell it what the value
|
||||
# type *would* be in this expression so that we can return a "NULL" of that type
|
||||
# its weird, but there isn't any other way that I can tell :)
|
||||
validate_type!(query, type, value)
|
||||
|
||||
dynamic =
|
||||
Ecto.Query.dynamic(type(^nil, ^type))
|
||||
|
||||
Ecto.Query.dynamic(fragment("ash_raise_error(?::jsonb, ?)", ^encoded, ^dynamic))
|
||||
else
|
||||
Ecto.Query.dynamic(fragment("ash_raise_error(?::jsonb)", ^encoded))
|
||||
end
|
||||
end
|
||||
|
||||
defp do_dynamic_expr(
|
||||
query,
|
||||
%Exists{at_path: at_path, path: [first | rest], expr: expr},
|
||||
|
|
162
lib/migration_generator/ash_functions.ex
Normal file
162
lib/migration_generator/ash_functions.ex
Normal file
|
@ -0,0 +1,162 @@
|
|||
defmodule AshPostgres.MigrationGenerator.AshFunctions do
|
||||
@latest_version 2
|
||||
|
||||
def latest_version, do: @latest_version
|
||||
|
||||
@moduledoc false
|
||||
def install(nil) do
|
||||
"""
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_or(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE)
|
||||
AS $$ SELECT COALESCE(NULLIF($1, FALSE), $2) $$
|
||||
LANGUAGE SQL
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_or(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE)
|
||||
AS $$ SELECT COALESCE($1, $2) $$
|
||||
LANGUAGE SQL
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_and(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$
|
||||
SELECT CASE
|
||||
WHEN $1 IS TRUE THEN $2
|
||||
ELSE $1
|
||||
END $$
|
||||
LANGUAGE SQL
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_and(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$
|
||||
SELECT CASE
|
||||
WHEN $1 IS NOT NULL THEN $2
|
||||
ELSE $1
|
||||
END $$
|
||||
LANGUAGE SQL
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_trim_whitespace(arr text[])
|
||||
RETURNS text[] AS $$
|
||||
DECLARE
|
||||
start_index INT = 1;
|
||||
end_index INT = array_length(arr, 1);
|
||||
BEGIN
|
||||
WHILE start_index <= end_index AND arr[start_index] = '' LOOP
|
||||
start_index := start_index + 1;
|
||||
END LOOP;
|
||||
|
||||
WHILE end_index >= start_index AND arr[end_index] = '' LOOP
|
||||
end_index := end_index - 1;
|
||||
END LOOP;
|
||||
|
||||
IF start_index > end_index THEN
|
||||
RETURN ARRAY[]::text[];
|
||||
ELSE
|
||||
RETURN arr[start_index : end_index];
|
||||
END IF;
|
||||
END; $$
|
||||
LANGUAGE plpgsql
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
|
||||
#{ash_raise_error()}
|
||||
"""
|
||||
end
|
||||
|
||||
def install(0) do
|
||||
"""
|
||||
execute(\"\"\"
|
||||
ALTER FUNCTION ash_elixir_or(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) IMMUTABLE
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
ALTER FUNCTION ash_elixir_or(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) IMMUTABLE
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
ALTER FUNCTION ash_elixir_and(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) IMMUTABLE
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
ALTER FUNCTION ash_elixir_and(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) IMMUTABLE
|
||||
\"\"\")
|
||||
|
||||
#{ash_raise_error()}
|
||||
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_trim_whitespace(arr text[])
|
||||
RETURNS text[] AS $$
|
||||
DECLARE
|
||||
start_index INT = 1;
|
||||
end_index INT = array_length(arr, 1);
|
||||
BEGIN
|
||||
WHILE start_index <= end_index AND arr[start_index] = '' LOOP
|
||||
start_index := start_index + 1;
|
||||
END LOOP;
|
||||
|
||||
WHILE end_index >= start_index AND arr[end_index] = '' LOOP
|
||||
end_index := end_index - 1;
|
||||
END LOOP;
|
||||
|
||||
IF start_index > end_index THEN
|
||||
RETURN ARRAY[]::text[];
|
||||
ELSE
|
||||
RETURN arr[start_index : end_index];
|
||||
END IF;
|
||||
END; $$
|
||||
LANGUAGE plpgsql
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
"""
|
||||
end
|
||||
|
||||
def install(1) do
|
||||
ash_raise_error()
|
||||
end
|
||||
|
||||
def drop(1) do
|
||||
"execute(\"DROP FUNCTION IF EXISTS ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE)\")"
|
||||
end
|
||||
|
||||
def drop(0) do
|
||||
"execute(\"DROP FUNCTION IF EXISTS ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE), ash_trim_whitespace(text[])\")"
|
||||
end
|
||||
|
||||
def drop(nil) do
|
||||
"execute(\"DROP FUNCTION IF EXISTS ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE), ash_elixir_and(BOOLEAN, ANYCOMPATIBLE), ash_elixir_and(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(BOOLEAN, ANYCOMPATIBLE) ash_trim_whitespace(text[])\")"
|
||||
end
|
||||
|
||||
defp ash_raise_error do
|
||||
"""
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
-- Raise an error with the provided JSON data.
|
||||
-- The JSON object is converted to text for inclusion in the error message.
|
||||
RAISE EXCEPTION '%', json_data::text;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_raise_error(json_data json, type_signal ANYCOMPATIBLE)
|
||||
RETURNS ANYCOMPATIBLE AS $$
|
||||
BEGIN
|
||||
-- Raise an error with the provided JSON data.
|
||||
-- The JSON object is converted to text for inclusion in the error message.
|
||||
RAISE EXCEPTION '%', json_data::text;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
\"\"\")
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -153,8 +153,6 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
Path.join([Mix.Project.deps_paths()[app] || File.cwd!(), "priv", "resource_snapshots"])
|
||||
end
|
||||
|
||||
@latest_ash_functions_version 1
|
||||
|
||||
defp create_extension_migrations(repos, opts) do
|
||||
for repo <- repos do
|
||||
snapshot_path = snapshot_path(opts, repo)
|
||||
|
@ -194,17 +192,15 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
|
||||
to_install =
|
||||
requesteds
|
||||
|> Enum.filter(fn {name, _extension} -> !Enum.member?(installed_extensions, name) end)
|
||||
|> Enum.map(fn {_name, extension} -> extension end)
|
||||
|> Enum.reject(fn
|
||||
{"ash-functions", _} ->
|
||||
extensions_snapshot[:ash_functions_version] ==
|
||||
AshPostgres.MigrationGenerator.AshFunctions.latest_version()
|
||||
|
||||
to_install =
|
||||
if "ash-functions" in requesteds &&
|
||||
extensions_snapshot[:ash_functions_version] !=
|
||||
@latest_ash_functions_version do
|
||||
Enum.uniq(["ash-functions" | to_install])
|
||||
else
|
||||
to_install
|
||||
end
|
||||
{name, _} ->
|
||||
Enum.member?(installed_extensions, name)
|
||||
end)
|
||||
|> Enum.map(fn {_name, extension} -> extension end)
|
||||
|
||||
if Enum.empty?(to_install) do
|
||||
Mix.shell().info("No extensions to install")
|
||||
|
@ -216,8 +212,9 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
{"install_#{ext_name}_v#{version}",
|
||||
"#{timestamp(true)}_install_#{ext_name}_v#{version}_extension"}
|
||||
|
||||
[single] ->
|
||||
{"install_#{single}", "#{timestamp(true)}_install_#{single}_extension"}
|
||||
["ash-functions" = single] ->
|
||||
{"install_#{single}_extension_#{AshPostgres.MigrationGenerator.AshFunctions.latest_version()}",
|
||||
"#{timestamp(true)}_install_#{single}_extension_#{AshPostgres.MigrationGenerator.AshFunctions.latest_version()}"}
|
||||
|
||||
multiple ->
|
||||
{"install_#{Enum.count(multiple)}_extensions",
|
||||
|
@ -239,7 +236,9 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
install =
|
||||
Enum.map_join(to_install, "\n", fn
|
||||
"ash-functions" ->
|
||||
install_ash_functions(extensions_snapshot[:ash_functions_version])
|
||||
AshPostgres.MigrationGenerator.AshFunctions.install(
|
||||
extensions_snapshot[:ash_functions_version]
|
||||
)
|
||||
|
||||
{_ext_name, version, up_fn, _down_fn} when is_function(up_fn, 1) ->
|
||||
up_fn.(version)
|
||||
|
@ -251,7 +250,9 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
uninstall =
|
||||
Enum.map_join(to_install, "\n", fn
|
||||
"ash-functions" ->
|
||||
"execute(\"DROP FUNCTION IF EXISTS ash_elixir_and(BOOLEAN, ANYCOMPATIBLE), ash_elixir_and(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(BOOLEAN, ANYCOMPATIBLE)\")"
|
||||
AshPostgres.MigrationGenerator.AshFunctions.drop(
|
||||
extensions_snapshot[:ash_functions_version]
|
||||
)
|
||||
|
||||
{_ext_name, version, _up_fn, down_fn} when is_function(down_fn, 1) ->
|
||||
down_fn.(version)
|
||||
|
@ -300,117 +301,13 @@ defmodule AshPostgres.MigrationGenerator do
|
|||
end
|
||||
end
|
||||
|
||||
defp install_ash_functions(nil) do
|
||||
"""
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_or(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE)
|
||||
AS $$ SELECT COALESCE(NULLIF($1, FALSE), $2) $$
|
||||
LANGUAGE SQL
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_or(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE)
|
||||
AS $$ SELECT COALESCE($1, $2) $$
|
||||
LANGUAGE SQL
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_and(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$
|
||||
SELECT CASE
|
||||
WHEN $1 IS TRUE THEN $2
|
||||
ELSE $1
|
||||
END $$
|
||||
LANGUAGE SQL
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_elixir_and(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$
|
||||
SELECT CASE
|
||||
WHEN $1 IS NOT NULL THEN $2
|
||||
ELSE $1
|
||||
END $$
|
||||
LANGUAGE SQL
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_trim_whitespace(arr text[])
|
||||
RETURNS text[] AS $$
|
||||
DECLARE
|
||||
start_index INT = 1;
|
||||
end_index INT = array_length(arr, 1);
|
||||
BEGIN
|
||||
WHILE start_index <= end_index AND arr[start_index] = '' LOOP
|
||||
start_index := start_index + 1;
|
||||
END LOOP;
|
||||
|
||||
WHILE end_index >= start_index AND arr[end_index] = '' LOOP
|
||||
end_index := end_index - 1;
|
||||
END LOOP;
|
||||
|
||||
IF start_index > end_index THEN
|
||||
RETURN ARRAY[]::text[];
|
||||
ELSE
|
||||
RETURN arr[start_index : end_index];
|
||||
END IF;
|
||||
END; $$
|
||||
LANGUAGE plpgsql
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
"""
|
||||
end
|
||||
|
||||
defp install_ash_functions(0) do
|
||||
"""
|
||||
execute(\"\"\"
|
||||
ALTER FUNCTION ash_elixir_or(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) IMMUTABLE
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
ALTER FUNCTION ash_elixir_or(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) IMMUTABLE
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
ALTER FUNCTION ash_elixir_and(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) IMMUTABLE
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
ALTER FUNCTION ash_elixir_and(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) IMMUTABLE
|
||||
\"\"\")
|
||||
|
||||
execute(\"\"\"
|
||||
CREATE OR REPLACE FUNCTION ash_trim_whitespace(arr text[])
|
||||
RETURNS text[] AS $$
|
||||
DECLARE
|
||||
start_index INT = 1;
|
||||
end_index INT = array_length(arr, 1);
|
||||
BEGIN
|
||||
WHILE start_index <= end_index AND arr[start_index] = '' LOOP
|
||||
start_index := start_index + 1;
|
||||
END LOOP;
|
||||
|
||||
WHILE end_index >= start_index AND arr[end_index] = '' LOOP
|
||||
end_index := end_index - 1;
|
||||
END LOOP;
|
||||
|
||||
IF start_index > end_index THEN
|
||||
RETURN ARRAY[]::text[];
|
||||
ELSE
|
||||
RETURN arr[start_index : end_index];
|
||||
END IF;
|
||||
END; $$
|
||||
LANGUAGE plpgsql
|
||||
IMMUTABLE;
|
||||
\"\"\")
|
||||
"""
|
||||
end
|
||||
|
||||
defp set_ash_functions(snapshot, installed_extensions) do
|
||||
if "ash-functions" in installed_extensions do
|
||||
Map.put(snapshot, :ash_functions_version, @latest_ash_functions_version)
|
||||
Map.put(
|
||||
snapshot,
|
||||
:ash_functions_version,
|
||||
AshPostgres.MigrationGenerator.AshFunctions.latest_version()
|
||||
)
|
||||
else
|
||||
snapshot
|
||||
end
|
||||
|
|
2
mix.exs
2
mix.exs
|
@ -204,7 +204,7 @@ defmodule AshPostgres.MixProject do
|
|||
{:ecto, "~> 3.9"},
|
||||
{:jason, "~> 1.0"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:ash, ash_version("~> 2.17 and >= 2.17.7")},
|
||||
{:ash, ash_version("~> 2.17 and >= 2.17.13")},
|
||||
{:benchee, "~> 1.1", only: [:dev, :test]},
|
||||
{:git_ops, "~> 2.5", only: [:dev, :test]},
|
||||
{:ex_doc, github: "elixir-lang/ex_doc", only: [:dev, :test], runtime: false},
|
||||
|
|
6
mix.lock
6
mix.lock
|
@ -1,5 +1,5 @@
|
|||
%{
|
||||
"ash": {:hex, :ash, "2.17.7", "8d8f359db61b8ed8245347d836f8a981e69fea769759c69468b5331b342f2308", [: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: false]}, {: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.50 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", "dd18cf96a245fed88b7bc1d715ab380aafb502e05bf7bc02e7f8200770d4335d"},
|
||||
"ash": {:hex, :ash, "2.17.13", "a3bb846238d4eb029da00583554f074d73950cfa4bc2f5964283d2e4491a9543", [: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: false]}, {: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.50 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", "b41a3e029b1553e71a0cab7db66d98a1ea7a883d301b866630ef9d09e64d38ee"},
|
||||
"benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"},
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||
|
@ -30,14 +30,14 @@
|
|||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
||||
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
||||
"postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{: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", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"},
|
||||
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
|
||||
"sourceror": {:hex, :sourceror, "0.14.1", "c6fb848d55bd34362880da671debc56e77fd722fa13b4dcbeac89a8998fc8b09", [:mix], [], "hexpm", "8b488a219e4c4d7d9ff29d16346fd4a5858085ccdd010e509101e226bbfd8efc"},
|
||||
"spark": {:hex, :spark, "1.1.51", "8458de5abbb89d18dd5c9235dd39e3757076eba84a5078d1cdc2c1e23c39aa95", [: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", "ed8410aa8db08867b8fff3d65e54deeb7f6f6cf2b8698fc405a386c1c7a9e4f0"},
|
||||
"spark": {:hex, :spark, "1.1.52", "e0ddd137899c11fb44ef46cda346a112e60365b93e50264da976f45b1c6e28c5", [: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", "2d8b354103eb4ae5fb4ed5f885d491e3ed5684ccb57806c3980fcc15a4b597d6"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
|
||||
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"ash_functions_version": 1,
|
||||
"installed": [
|
||||
"ash-functions",
|
||||
"uuid-ossp",
|
||||
"pg_trgm",
|
||||
"citext",
|
||||
"demo-functions_v1"
|
||||
]
|
||||
],
|
||||
"ash_functions_version": 2
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
defmodule AshPostgres.TestRepo.Migrations.InstallAshFunctionsExtension2 do
|
||||
@moduledoc """
|
||||
Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
-- Raise an error with the provided JSON data.
|
||||
-- The JSON object is converted to text for inclusion in the error message.
|
||||
RAISE EXCEPTION '%', json_data::text;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
""")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION ash_raise_error(json_data json, type_signal ANYCOMPATIBLE)
|
||||
RETURNS ANYCOMPATIBLE AS $$
|
||||
BEGIN
|
||||
-- Raise an error with the provided JSON data.
|
||||
-- The JSON object is converted to text for inclusion in the error message.
|
||||
RAISE EXCEPTION '%', json_data::text;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
""")
|
||||
end
|
||||
|
||||
def down do
|
||||
# Uncomment this if you actually want to uninstall the extensions
|
||||
# when this migration is rolled back:
|
||||
execute(
|
||||
"DROP FUNCTION IF EXISTS ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE)"
|
||||
)
|
||||
end
|
||||
end
|
38
test/error_expr_test.ex
Normal file
38
test/error_expr_test.ex
Normal file
|
@ -0,0 +1,38 @@
|
|||
defmodule AshPostgres.ErrorExprTest do
|
||||
use AshPostgres.RepoCase, async: false
|
||||
alias AshPostgres.Test.{Api, Author, Comment, Post}
|
||||
|
||||
require Ash.Query
|
||||
import Ash.Expr
|
||||
|
||||
test "exceptions in filters are treated as regular Ash exceptions" do
|
||||
Post
|
||||
|> Ash.Changeset.new(%{title: "title"})
|
||||
|> Api.create!()
|
||||
|
||||
assert_raise Ash.Error.Invalid, ~r/this is bad!/, fn ->
|
||||
Post
|
||||
|> Ash.Query.filter(
|
||||
error(Ash.Error.Query.InvalidFilterValue, message: "this is bad!", value: 10)
|
||||
)
|
||||
|> Api.read!()
|
||||
end
|
||||
end
|
||||
|
||||
test "exceptions in calculations are treated as regular Ash exceptions" do
|
||||
Post
|
||||
|> Ash.Changeset.new(%{title: "title"})
|
||||
|> Api.create!()
|
||||
|
||||
assert_raise Ash.Error.Invalid, ~r/this is bad!/, fn ->
|
||||
Post
|
||||
|> Ash.Query.calculate(
|
||||
:test,
|
||||
expr(error(Ash.Error.Query.InvalidFilterValue, message: "this is bad!", value: 10)),
|
||||
:string
|
||||
)
|
||||
|> Api.read!()
|
||||
|> Enum.map(& &1.calculations)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue