improvement: support for error/2 expression

This commit is contained in:
Zach Daniel 2023-12-14 17:10:11 -05:00
parent 54908395eb
commit 407a7163ed
9 changed files with 476 additions and 228 deletions

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
{
"ash_functions_version": 1,
"installed": [
"ash-functions",
"uuid-ossp",
"pg_trgm",
"citext",
"demo-functions_v1"
]
],
"ash_functions_version": 2
}

View file

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