Packages

ash_postgres

2.6.29
2.10.0 2.9.1 2.9.0 2.8.0 2.7.0 2.6.32 2.6.31 2.6.30 2.6.29 2.6.28 2.6.27 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.22 2.5.21 2.5.20 2.5.19 2.5.18 2.5.17 2.5.16 2.5.15 2.5.14 2.5.13 2.5.12 2.5.11 2.5.10 2.5.9 2.5.8 2.5.7 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.4.21 2.4.20 2.4.19 2.4.18 2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 2.3.1 2.3.0 2.2.5 2.2.4 2.2.3 2.2.2 2.2.1 2.2.0 2.1.19 2.1.18 2.1.17 2.1.15 2.1.14 2.1.13 2.1.12 2.1.11 2.1.10 2.1.9 2.1.8 2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.12 2.0.11 2.0.10 2.0.9 2.0.8 2.0.7 2.0.6 2.0.5 2.0.4 2.0.3 2.0.2 2.0.1 2.0.0 2.0.0-rc.15 2.0.0-rc.14 2.0.0-rc.13 2.0.0-rc.12 2.0.0-rc.11 2.0.0-rc.10 2.0.0-rc.9 2.0.0-rc.8 2.0.0-rc.7 2.0.0-rc.6 2.0.0-rc.5 2.0.0-rc.4 2.0.0-rc.3 2.0.0-rc.2 2.0.0-rc.1 2.0.0-rc.0 1.5.30 1.5.29 1.5.28 1.5.27 1.5.26 1.5.25 1.5.24 1.5.23 1.5.22 1.5.21 1.5.20 1.5.19 1.5.18 1.5.17 1.5.16 1.5.15 1.5.14 1.5.13 1.5.12 1.5.11 1.5.10 1.5.9 1.5.8 1.5.7 1.5.6 1.5.5 1.5.4 1.5.3 1.5.2 1.5.1 1.5.0 1.4.0 1.3.68 1.3.67 1.3.66 1.3.65 1.3.64 1.3.63 1.3.62 1.3.61 1.3.60 1.3.59 1.3.58 1.3.56 1.3.55 1.3.54 1.3.53 1.3.52 1.3.51 1.3.50 1.3.49 1.3.48 1.3.47 1.3.46 1.3.45 1.3.44 1.3.43 1.3.42 1.3.41 1.3.40 1.3.39 1.3.38 1.3.37 1.3.36 1.3.35 1.3.34 1.3.33 1.3.32 1.3.31 1.3.30 1.3.29 1.3.28 1.3.26 1.3.25 1.3.24 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 1.3.18 1.3.17 1.3.16 1.3.15 1.3.14 1.3.12 1.3.11 1.3.10 1.3.9 1.3.8 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.3.0-rc.4 1.3.0-rc.3 1.3.0-rc.2 1.3.0-rc.0 1.2.6 1.2.5 1.2.4 1.2.3 1.2.2 1.2.1 1.2.0 1.2.0-rc.1 1.2.0-rc.0 1.1.3 1.1.2 1.1.1 1.1.0 1.0.0 1.0.0-rc.9 1.0.0-rc.8 1.0.0-rc.7 1.0.0-rc.6 1.0.0-rc.5 1.0.0-rc.4 1.0.0-rc.3 1.0.0-rc.2 1.0.0-rc.1 1.0.0-rc.0 1.0.0-pre.3 1.0.0-pre.2 1.0.0-pre.1 1.0.0-pre.0 0.43.0 0.42.0-rc.7 0.42.0-rc.6 0.42.0-rc.5 0.42.0-rc.4 0.42.0-rc.3 0.42.0-rc.1 0.42.0-rc.0 0.41.7 0.41.6 0.41.5 0.41.4 0.41.3 0.41.2 0.41.1 0.41.0-rc0 retired 0.41.0-rc.9 0.41.0-rc.8 0.41.0-rc.7 0.41.0-rc.6 0.41.0-rc.5 0.41.0-rc.4 0.41.0-rc.3 0.41.0-rc.2 0.41.0-rc.1 0.41.0-rc.0 0.40.11 0.40.10 0.40.9 0.40.8 0.40.7 0.40.6 0.40.5 0.40.4 0.40.3 0.40.2 0.40.1 0.40.0-rc5 0.40.0-rc4 0.40.0-rc3 0.40.0-rc2 0.40.0-rc1 0.39.0-rc0 0.38.11 0.38.10 0.38.9 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.8 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.5 0.35.4 0.35.3 0.35.1 0.34.5 0.34.4 0.34.3 0.34.2 0.34.1 0.34.0 0.33.1 0.33.0 0.32.2 0.32.1 0.32.0 0.31.1 0.30.1 0.29.6 0.29.5 0.29.4 0.29.3 0.29.2 0.29.1 0.28.1 0.28.0 0.27.0 0.26.2 0.26.1 0.26.0 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.0 0.23.2 0.23.1 0.23.0 0.22.1 0.22.0 0.21.0 0.20.1 0.20.0 0.19.0 0.18.0 0.17.0 0.16.1 0.15.0 0.14.0 0.13.0 0.12.1 0.12.0 0.11.2 0.10.0 0.9.0 0.8.0 0.7.0 0.6.0 0.5.0 0.4.0 0.3.0 0.2.1 0.2.0 0.1.4 0.1.3 0.1.2 0.1.1 0.1.1-rc.2 0.1.1-rc.1 0.1.1-rc.0 0.1.0

The PostgreSQL data layer for Ash Framework

Current section

Files

Jump to
ash_postgres lib extensions immutable_raise_error.ex
Raw

lib/extensions/immutable_raise_error.ex

# SPDX-FileCopyrightText: 2019 ash_postgres contributors <https://github.com/ash-project/ash_postgres/graphs.contributors>
#
# SPDX-License-Identifier: MIT
defmodule AshPostgres.Extensions.ImmutableRaiseError do
@moduledoc """
An extension that installs an immutable version of ash_raise_error.
This can be used to improve compatibility with Postgres sharding extensions like Citus,
which requires functions used in CASE or COALESCE expressions to be immutable.
The new `ash_raise_error_immutable` functions add an additional row-dependent argument to ensure
the planner doesn't constant-fold error expressions.
To install, add this module to your repo's `installed_extensions` list:
```elixir
def installed_extensions do
["ash-functions", AshPostgres.Extensions.ImmutableRaiseError]
end
```
And run `mix ash_postgres.generate_migrations` to generate the migrations.
Once installed, you can control whether the immutable function is used by adding this to your
repo:
```elixir
def immutable_expr_error?, do: true
```
"""
use AshPostgres.CustomExtension, name: "immutable_raise_error", latest_version: 2
require Ecto.Query
@impl true
def install(0) do
"""
#{ash_raise_error_immutable()}
#{ash_to_jsonb_immutable()}
"""
end
def install(1) do
ash_to_jsonb_immutable()
end
@impl true
def uninstall(2) do
"execute(\"DROP FUNCTION IF EXISTS ash_to_jsonb_immutable(anyelement)\")"
end
def uninstall(_version) do
"execute(\"DROP FUNCTION IF EXISTS ash_to_jsonb_immutable(anyelement), ash_raise_error_immutable(jsonb, anycompatible), ash_raise_error_immutable(jsonb, anyelement, anycompatible)\")"
end
defp ash_raise_error_immutable do
"""
execute(\"\"\"
CREATE OR REPLACE FUNCTION ash_raise_error_immutable(json_data jsonb, token anycompatible)
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.
-- 'token' is intentionally ignored; its presence makes the call non-constant at the call site.
RAISE EXCEPTION 'ash_error: %', json_data::text;
RETURN NULL;
END;
$$ LANGUAGE plpgsql
IMMUTABLE
SET search_path = '';
\"\"\")
execute(\"\"\"
CREATE OR REPLACE FUNCTION ash_raise_error_immutable(json_data jsonb, type_signal anyelement, token anycompatible)
RETURNS anyelement AS $$
BEGIN
-- Raise an error with the provided JSON data.
-- The JSON object is converted to text for inclusion in the error message.
-- 'token' is intentionally ignored; its presence makes the call non-constant at the call site.
RAISE EXCEPTION 'ash_error: %', json_data::text;
RETURN NULL;
END;
$$ LANGUAGE plpgsql
IMMUTABLE
SET search_path = '';
\"\"\")
"""
end
# Wraps to_jsonb and pins session GUCs that affect JSON. This makes the function’s result
# deterministic, so it is safe to mark IMMUTABLE.
defp ash_to_jsonb_immutable do
"""
execute(\"\"\"
CREATE OR REPLACE FUNCTION ash_to_jsonb_immutable(value anyelement)
RETURNS jsonb
LANGUAGE plpgsql
IMMUTABLE
SET search_path TO 'pg_catalog'
SET \"TimeZone\" TO 'UTC'
SET \"DateStyle\" TO 'ISO, YMD'
SET \"IntervalStyle\" TO 'iso_8601'
SET extra_float_digits TO '0'
SET bytea_output TO 'hex'
AS $function$
BEGIN
RETURN COALESCE(to_jsonb(value), 'null'::jsonb);
END;
$function$
\"\"\")
"""
end
@doc false
def immutable_error_expr(
query,
%Ash.Query.Function.Error{arguments: [exception, input]} = value,
bindings,
_embedded?,
acc,
type
) do
if !(Keyword.keyword?(input) or is_map(input)) do
raise "Input expression to `error` must be a map or keyword list"
end
acc = %{acc | has_error?: true}
{error_payload, acc} =
if Ash.Expr.expr?(input) do
expression_error_payload(exception, input, query, bindings, acc)
else
{Jason.encode!(%{exception: inspect(exception), input: Map.new(input)}), acc}
end
dynamic_type =
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 :)
AshSql.Expr.validate_type!(query, type, value)
type =
AshSql.Expr.parameterized_type(
bindings.sql_behaviour,
type,
[],
:expr
)
Ecto.Query.dynamic(type(fragment("NULL"), ^type))
else
nil
end
case {dynamic_type, immutable_error_expr_token(query, bindings)} do
{_, nil} ->
:error
{nil, row_token} ->
{:ok,
Ecto.Query.dynamic(
fragment("ash_raise_error_immutable(?::jsonb, ?)", ^error_payload, ^row_token)
), acc}
{dynamic_type, row_token} ->
{:ok,
Ecto.Query.dynamic(
fragment(
"ash_raise_error_immutable(?::jsonb, ?, ?)",
^error_payload,
^dynamic_type,
^row_token
)
), acc}
end
end
# Encodes an error payload as jsonb using only IMMUTABLE SQL functions.
#
# Strategy:
# * Split the 'input' into Ash expressions and literal values
# * Build the base json map with the exception name and literal input values
# * For each expression value, use nested calls to `jsonb_set` (IMMUTABLE) to add the value to
# 'input', converting each expression to jsonb using `ash_to_jsonb_immutable` (which pins
# session GUCs for deterministic encoding)
defp expression_error_payload(exception, input, query, bindings, acc) do
{expr_inputs, literal_inputs} =
Enum.split_with(input, fn {_key, value} -> Ash.Expr.expr?(value) end)
base_json = %{exception: inspect(exception), input: Map.new(literal_inputs)}
Enum.reduce(expr_inputs, {base_json, acc}, fn
{key, expr_value}, {current_payload, acc} ->
path_expr = %Ash.Query.Function.Type{
arguments: [["input", to_string(key)], {:array, :string}, []]
}
new_value_jsonb =
%Ash.Query.Function.Fragment{
arguments: [raw: "ash_to_jsonb_immutable(", expr: expr_value, raw: ")"]
}
{%Ecto.Query.DynamicExpr{} = new_payload, acc} =
AshSql.Expr.dynamic_expr(
query,
%Ash.Query.Function.Fragment{
arguments: [
raw: "jsonb_set(",
expr: current_payload,
raw: "::jsonb, ",
expr: path_expr,
raw: ", ",
expr: new_value_jsonb,
raw: "::jsonb, true)"
]
},
bindings,
false,
nil,
acc
)
{new_payload, acc}
end)
end
# Returns a row-dependent token to prevent constant-folding for immutable functions.
defp immutable_error_expr_token(query, bindings) do
resource = query.__ash_bindings__.resource
ref_binding = bindings.root_binding
pk_attr_names = Ash.Resource.Info.primary_key(resource)
attr_names =
case pk_attr_names do
[] ->
case Ash.Resource.Info.attributes(resource) do
[%{name: name} | _] -> [name]
_ -> []
end
pk ->
pk
end
if ref_binding && attr_names != [] do
value_exprs =
Enum.map(attr_names, fn attr_name ->
if bindings[:parent?] &&
ref_binding not in List.wrap(bindings[:lateral_join_bindings]) do
Ecto.Query.dynamic(field(parent_as(^ref_binding), ^attr_name))
else
Ecto.Query.dynamic(field(as(^ref_binding), ^attr_name))
end
end)
row_parts =
value_exprs
|> Enum.map(&{:casted_expr, &1})
|> Enum.intersperse({:raw, ", "})
{%Ecto.Query.DynamicExpr{} = token, _acc} =
AshSql.Expr.dynamic_expr(
query,
%Ash.Query.Function.Fragment{
embedded?: false,
arguments: [raw: "ROW("] ++ row_parts ++ [raw: ")"]
},
AshSql.Expr.set_location(bindings, :sub_expr),
false
)
token
else
nil
end
end
end