Skip to content

Commit cf0d1df

Browse files
refactor: move immutable error expr from AshSql into AshPostgres (#633)
1 parent c45b336 commit cf0d1df

File tree

4 files changed

+181
-7
lines changed

4 files changed

+181
-7
lines changed

lib/extensions/immutable_raise_error.ex

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ defmodule AshPostgres.Extensions.ImmutableRaiseError do
2828

2929
use AshPostgres.CustomExtension, name: "immutable_raise_error", latest_version: 1
3030

31+
require Ecto.Query
32+
3133
@impl true
3234
def install(0) do
3335
ash_raise_error_immutable()
@@ -71,4 +73,157 @@ defmodule AshPostgres.Extensions.ImmutableRaiseError do
7173
\"\"\")
7274
"""
7375
end
76+
77+
@doc false
78+
def immutable_error_expr(
79+
query,
80+
%Ash.Query.Function.Error{arguments: [exception, input]} = value,
81+
bindings,
82+
embedded?,
83+
acc,
84+
type
85+
) do
86+
acc = %{acc | has_error?: true}
87+
88+
{encoded, acc} =
89+
if Ash.Expr.expr?(input) do
90+
frag_parts =
91+
Enum.flat_map(input, fn {key, value} ->
92+
if Ash.Expr.expr?(value) do
93+
[
94+
expr: to_string(key),
95+
raw: "::text, ",
96+
expr: value,
97+
raw: ", "
98+
]
99+
else
100+
[
101+
expr: to_string(key),
102+
raw: "::text, ",
103+
expr: value,
104+
raw: "::jsonb, "
105+
]
106+
end
107+
end)
108+
109+
frag_parts =
110+
List.update_at(frag_parts, -1, fn {:raw, text} ->
111+
{:raw, String.trim_trailing(text, ", ") <> "))"}
112+
end)
113+
114+
AshSql.Expr.dynamic_expr(
115+
query,
116+
%Ash.Query.Function.Fragment{
117+
embedded?: false,
118+
arguments:
119+
[
120+
raw: "jsonb_build_object('exception', ",
121+
expr: inspect(exception),
122+
raw: "::text, 'input', jsonb_build_object("
123+
] ++
124+
frag_parts
125+
},
126+
bindings,
127+
embedded?,
128+
nil,
129+
acc
130+
)
131+
else
132+
{Jason.encode!(%{exception: inspect(exception), input: Map.new(input)}), acc}
133+
end
134+
135+
dynamic_type =
136+
if type do
137+
# This is a type hint, if we're raising an error, we tell it what the value
138+
# type *would* be in this expression so that we can return a "NULL" of that type
139+
# its weird, but there isn't any other way that I can tell :)
140+
AshSql.Expr.validate_type!(query, type, value)
141+
142+
type =
143+
AshSql.Expr.parameterized_type(
144+
bindings.sql_behaviour,
145+
type,
146+
[],
147+
:expr
148+
)
149+
150+
Ecto.Query.dynamic(type(fragment("NULL"), ^type))
151+
else
152+
nil
153+
end
154+
155+
case {dynamic_type, immutable_error_expr_token(query, bindings)} do
156+
{_, nil} ->
157+
:error
158+
159+
{nil, row_token} ->
160+
{:ok,
161+
Ecto.Query.dynamic(
162+
fragment("ash_raise_error_immutable(?::jsonb, ?)", ^encoded, ^row_token)
163+
), acc}
164+
165+
{dynamic_type, row_token} ->
166+
{:ok,
167+
Ecto.Query.dynamic(
168+
fragment(
169+
"ash_raise_error_immutable(?::jsonb, ?, ?)",
170+
^encoded,
171+
^dynamic_type,
172+
^row_token
173+
)
174+
), acc}
175+
end
176+
end
177+
178+
# Returns a row-dependent token to prevent constant-folding for immutable functions.
179+
defp immutable_error_expr_token(query, bindings) do
180+
resource = query.__ash_bindings__.resource
181+
ref_binding = bindings.root_binding
182+
183+
pk_attr_names = Ash.Resource.Info.primary_key(resource)
184+
185+
attr_names =
186+
case pk_attr_names do
187+
[] ->
188+
case Ash.Resource.Info.attributes(resource) do
189+
[%{name: name} | _] -> [name]
190+
_ -> []
191+
end
192+
193+
pk ->
194+
pk
195+
end
196+
197+
if ref_binding && attr_names != [] do
198+
value_exprs =
199+
Enum.map(attr_names, fn attr_name ->
200+
if bindings[:parent?] &&
201+
ref_binding not in List.wrap(bindings[:lateral_join_bindings]) do
202+
Ecto.Query.dynamic(field(parent_as(^ref_binding), ^attr_name))
203+
else
204+
Ecto.Query.dynamic(field(as(^ref_binding), ^attr_name))
205+
end
206+
end)
207+
208+
row_parts =
209+
value_exprs
210+
|> Enum.map(&{:casted_expr, &1})
211+
|> Enum.intersperse({:raw, ", "})
212+
213+
{%Ecto.Query.DynamicExpr{} = token, _acc} =
214+
AshSql.Expr.dynamic_expr(
215+
query,
216+
%Ash.Query.Function.Fragment{
217+
embedded?: false,
218+
arguments: [raw: "ROW("] ++ row_parts ++ [raw: ")"]
219+
},
220+
AshSql.Expr.set_location(bindings, :sub_expr),
221+
false
222+
)
223+
224+
token
225+
else
226+
nil
227+
end
228+
end
74229
end

lib/sql_implementation.ex

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,31 @@ defmodule AshPostgres.SqlImplementation do
200200
end
201201
end
202202

203+
def expr(
204+
query,
205+
%Ash.Query.Function.Error{} = value,
206+
bindings,
207+
embedded?,
208+
acc,
209+
type
210+
) do
211+
resource = query.__ash_bindings__.resource
212+
repo = AshSql.dynamic_repo(resource, AshPostgres.SqlImplementation, query)
213+
214+
if repo.immutable_expr_error?() do
215+
AshPostgres.Extensions.ImmutableRaiseError.immutable_error_expr(
216+
query,
217+
value,
218+
bindings,
219+
embedded?,
220+
acc,
221+
type
222+
)
223+
else
224+
:error
225+
end
226+
end
227+
203228
def expr(
204229
_query,
205230
_expr,
@@ -334,9 +359,4 @@ defmodule AshPostgres.SqlImplementation do
334359

335360
{types, new_returns || returns}
336361
end
337-
338-
@impl true
339-
def immutable_errors?(repo) do
340-
repo.immutable_expr_error?()
341-
end
342362
end

mix.exs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,6 @@ defmodule AshPostgres.MixProject do
168168
[
169169
{:ash, ash_version("~> 3.5 and >= 3.5.35")},
170170
{:spark, "~> 2.3 and >= 2.3.4"},
171-
# TODO: bump to next ash_sql release
172171
{:ash_sql, ash_sql_version(git: "https://github.com/ash-project/ash_sql.git")},
173172
{:igniter, "~> 0.6 and >= 0.6.14", optional: true},
174173
{:ecto_sql, "~> 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": {:git, "https://github.com/ash-project/ash_sql.git", "65854408e7ce129f78fabafb0a4393f0142da6a6", []},
3+
"ash_sql": {:git, "https://github.com/ash-project/ash_sql.git", "3044c0555dbe6733d16868951ee89e6d5ef336fa", []},
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)