Skip to content

Commit 5c4a429

Browse files
improvement: Add immutable version of ash_raise_error function to support extensions like Citus (#620)
1 parent 14581be commit 5c4a429

File tree

5 files changed

+93
-3
lines changed

5 files changed

+93
-3
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,4 +334,9 @@ defmodule AshPostgres.SqlImplementation do
334334

335335
{types, new_returns || returns}
336336
end
337+
338+
@impl true
339+
def immutable_errors?(repo) do
340+
repo.immutable_expr_error?()
341+
end
337342
end

mix.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ defmodule AshPostgres.MixProject do
168168
[
169169
{:ash, ash_version("~> 3.5 and >= 3.5.35")},
170170
{:spark, "~> 2.3 and >= 2.3.4"},
171-
{:ash_sql, ash_sql_version("~> 0.2 and >= 0.2.90")},
171+
# TODO: bump to next ash_sql release
172+
# {:ash_sql, ash_sql_version("~> 0.2 and >= 0.2.90")},
173+
{:ash_sql, git: "https://github.com/ash-project/ash_sql.git"},
172174
{:igniter, "~> 0.6 and >= 0.6.14", optional: true},
173175
{:ecto_sql, "~> 3.13"},
174176
{:ecto, "~> 3.13"},

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
%{
22
"ash": {:hex, :ash, "3.5.43", "222f9a8ac26ad3b029f8e69306cc83091c992d858b4538af12e33a148f301cab", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "48b2aa274c524f5b968c563dd56aec8f9b278c529c8aa46e6fe0ca564c26cc1c"},
3-
"ash_sql": {:hex, :ash_sql, "0.3.0", "2c43ddcc8c7fb51dc25ba3bca965d8b68e7aaecb290cabfed3cf213965aca937", [:mix], [{:ash, ">= 3.5.43 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "ef03759d6a1d4cb189fcadbd183cf047f0565d7d88ea8612d85c9e6e724835e7"},
3+
"ash_sql": {:git, "https://github.com/ash-project/ash_sql.git", "65854408e7ce129f78fabafb0a4393f0142da6a6", []},
44
"benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"},
55
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
66
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},

0 commit comments

Comments
 (0)