Skip to content

Commit ab4b99a

Browse files
improvement: add ImmutableRaiseError extension
1 parent 1130a02 commit ab4b99a

File tree

3 files changed

+92
-1
lines changed

3 files changed

+92
-1
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
defmodule AshPostgres.Extensions.ImmutableRaiseError do
2+
@moduledoc """
3+
An extension that installs an immutable version of ash_raise_error.
4+
5+
This can be used to improve compatibility with Postgres sharding extensions like Citus,
6+
which requires functions used in CASE or COALESCE expressions to be immutable.
7+
8+
The new `ash_raise_error_immutable` functions add an additional row-dependent argument to ensure
9+
the planner doesn't constant-fold error expressions.
10+
11+
To install, add this module to your repo's `installed_extensions` list:
12+
13+
```elixir
14+
def installed_extensions do
15+
["ash-functions", AshPostgres.Extensions.ImmutableRaiseError]
16+
end
17+
```
18+
19+
And run `mix ash_postgres.generate_migrations` to generate the migrations.
20+
21+
Once installed, you can control whether the immutable function is used by adding this to your
22+
repo:
23+
24+
```elixir
25+
def immutable_expr_error?, do: true
26+
```
27+
"""
28+
29+
use AshPostgres.CustomExtension, name: "immutable_raise_error", latest_version: 1
30+
31+
@impl true
32+
def install(0) do
33+
ash_raise_error_immutable()
34+
end
35+
36+
@impl true
37+
def uninstall(_version) do
38+
"execute(\"DROP FUNCTION IF EXISTS ash_raise_error_immutable(jsonb, ANYCOMPATIBLE), ash_raise_error_immutable(jsonb, ANYELEMENT, ANYCOMPATIBLE)\")"
39+
end
40+
41+
defp ash_raise_error_immutable do
42+
"""
43+
execute(\"\"\"
44+
CREATE OR REPLACE FUNCTION ash_raise_error_immutable(json_data jsonb, token ANYCOMPATIBLE)
45+
RETURNS BOOLEAN AS $$
46+
BEGIN
47+
-- Raise an error with the provided JSON data.
48+
-- The JSON object is converted to text for inclusion in the error message.
49+
-- 'token' is intentionally ignored; its presence makes the call non-constant at the call site.
50+
RAISE EXCEPTION 'ash_error: %', json_data::text;
51+
RETURN NULL;
52+
END;
53+
$$ LANGUAGE plpgsql
54+
IMMUTABLE
55+
SET search_path = '';
56+
\"\"\")
57+
58+
execute(\"\"\"
59+
CREATE OR REPLACE FUNCTION ash_raise_error_immutable(json_data jsonb, type_signal ANYELEMENT, token ANYCOMPATIBLE)
60+
RETURNS ANYELEMENT AS $$
61+
BEGIN
62+
-- Raise an error with the provided JSON data.
63+
-- The JSON object is converted to text for inclusion in the error message.
64+
-- 'token' is intentionally ignored; its presence makes the call non-constant at the call site.
65+
RAISE EXCEPTION 'ash_error: %', json_data::text;
66+
RETURN NULL;
67+
END;
68+
$$ LANGUAGE plpgsql
69+
IMMUTABLE
70+
SET search_path = '';
71+
\"\"\")
72+
"""
73+
end
74+
end

lib/repo.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ defmodule AshPostgres.Repo do
113113
@doc "Disable expression errors for this repo"
114114
@callback disable_expr_error?() :: boolean
115115

116+
@doc """
117+
Opt-in to using immutable versions of the expression error functions.
118+
119+
Requires the `AshPostgres.Extensions.ImmutableRaiseError` extension.
120+
"""
121+
@callback immutable_expr_error?() :: boolean
122+
116123
defmacro __using__(opts) do
117124
quote bind_quoted: [opts: opts] do
118125
if Keyword.get(opts, :define_ecto_repo?, true) do
@@ -145,6 +152,7 @@ defmodule AshPostgres.Repo do
145152
def drop?, do: true
146153
def disable_atomic_actions?, do: false
147154
def disable_expr_error?, do: false
155+
def immutable_expr_error?, do: false
148156

149157
# default to false in 4.0
150158
def prefer_transaction?, do: true
@@ -315,7 +323,8 @@ defmodule AshPostgres.Repo do
315323
create?: 0,
316324
drop?: 0,
317325
disable_atomic_actions?: 0,
318-
disable_expr_error?: 0
326+
disable_expr_error?: 0,
327+
immutable_expr_error?: 0
319328

320329
# We do this switch because `!@warn_on_missing_ash_functions` in the function body triggers
321330
# a dialyzer error

lib/sql_implementation.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,4 +334,12 @@ defmodule AshPostgres.SqlImplementation do
334334

335335
{types, new_returns || returns}
336336
end
337+
338+
# Conditionally apply @impl to keep compatibility across ash_sql versions
339+
if {:immutable_errors?, 1} in AshSql.Implementation.behaviour_info(:callbacks) do
340+
@impl true
341+
def immutable_errors?(repo) do
342+
repo.immutable_expr_error?()
343+
end
344+
end
337345
end

0 commit comments

Comments
 (0)