Skip to content

Commit 6a2675c

Browse files
test: add repro for ash_sql issue (#629)
1 parent 5c4a429 commit 6a2675c

File tree

6 files changed

+190
-0
lines changed

6 files changed

+190
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"attributes": [
3+
{
4+
"allow_nil?": false,
5+
"default": "fragment(\"gen_random_uuid()\")",
6+
"generated?": false,
7+
"precision": null,
8+
"primary_key?": true,
9+
"references": null,
10+
"scale": null,
11+
"size": null,
12+
"source": "id",
13+
"type": "uuid"
14+
},
15+
{
16+
"allow_nil?": false,
17+
"default": "0",
18+
"generated?": false,
19+
"precision": null,
20+
"primary_key?": false,
21+
"references": null,
22+
"scale": null,
23+
"size": null,
24+
"source": "response",
25+
"type": "integer"
26+
}
27+
],
28+
"base_filter": null,
29+
"check_constraints": [],
30+
"custom_indexes": [],
31+
"custom_statements": [],
32+
"has_create_action": true,
33+
"hash": "8ADC3A631361A18B1B9A2070D5E8477428EDE39DE3B43FA5FF8E50CE7710B9E5",
34+
"identities": [],
35+
"multitenancy": {
36+
"attribute": null,
37+
"global": null,
38+
"strategy": null
39+
},
40+
"repo": "Elixir.AshPostgres.TestRepo",
41+
"schema": null,
42+
"table": "rsvps"
43+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
defmodule AshPostgres.TestRepo.Migrations.MigrateResources62 do
2+
@moduledoc """
3+
Updates resources based on their most recent snapshots.
4+
5+
This file was autogenerated with `mix ash_postgres.generate_migrations`
6+
"""
7+
8+
use Ecto.Migration
9+
10+
def up do
11+
create table(:rsvps, primary_key: false) do
12+
add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true)
13+
add(:response, :integer, null: false, default: 0)
14+
end
15+
end
16+
17+
def down do
18+
drop(table(:rsvps))
19+
end
20+
end

test/support/domain.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ defmodule AshPostgres.Test.Domain do
5353
resource(AshPostgres.Test.Order)
5454
resource(AshPostgres.Test.Chat)
5555
resource(AshPostgres.Test.Message)
56+
resource(AshPostgres.Test.RSVP)
5657
end
5758

5859
authorization do

test/support/resources/rsvp.ex

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
defmodule AshPostgres.Test.RSVP do
2+
@moduledoc false
3+
use Ash.Resource,
4+
domain: AshPostgres.Test.Domain,
5+
data_layer: AshPostgres.DataLayer
6+
7+
postgres do
8+
table "rsvps"
9+
repo AshPostgres.TestRepo
10+
end
11+
12+
actions do
13+
default_accept(:*)
14+
defaults([:create, :read, :update, :destroy])
15+
16+
# Uses an expression with an array of atoms for a custom type backed by integers.
17+
update :clear_response do
18+
change(
19+
atomic_update(
20+
:response,
21+
expr(
22+
if response in [:accepted, :declined] do
23+
:awaiting
24+
else
25+
response
26+
end
27+
)
28+
)
29+
)
30+
end
31+
end
32+
33+
attributes do
34+
uuid_primary_key(:id)
35+
36+
attribute(:response, AshPostgres.Test.Types.Response,
37+
allow_nil?: false,
38+
public?: true,
39+
default: 0
40+
)
41+
end
42+
end

test/support/types/response.ex

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
defmodule AshPostgres.Test.Types.Response do
2+
@moduledoc false
3+
use Ash.Type
4+
use AshPostgres.Type
5+
require Ash.Expr
6+
7+
@atoms_to_ints %{accepted: 1, declined: 2, awaiting: 0}
8+
@ints_to_atoms Map.new(@atoms_to_ints, fn {k, v} -> {v, k} end)
9+
@atom_values Map.keys(@atoms_to_ints)
10+
@string_values Enum.map(@atom_values, &to_string/1)
11+
12+
@impl Ash.Type
13+
def storage_type, do: :integer
14+
15+
@impl Ash.Type
16+
def cast_input(nil, _), do: {:ok, nil}
17+
18+
def cast_input(value, _) when value in @atom_values, do: {:ok, value}
19+
def cast_input(value, _) when value in @string_values, do: {:ok, String.to_existing_atom(value)}
20+
21+
def cast_input(integer, _) when is_integer(integer),
22+
do: Map.fetch(@ints_to_atoms, integer)
23+
24+
def cast_input(_, _), do: :error
25+
26+
@impl Ash.Type
27+
def matches_type?(value, _) when is_atom(value) and value in @atom_values, do: true
28+
def matches_type?(_, _), do: false
29+
30+
@impl Ash.Type
31+
def cast_stored(nil, _), do: {:ok, nil}
32+
def cast_stored(integer, _) when is_integer(integer), do: Map.fetch(@ints_to_atoms, integer)
33+
def cast_stored(_, _), do: :error
34+
35+
@impl Ash.Type
36+
def dump_to_native(nil, _), do: {:ok, nil}
37+
def dump_to_native(atom, _) when is_atom(atom), do: Map.fetch(@atoms_to_ints, atom)
38+
def dump_to_native(_, _), do: :error
39+
40+
@impl Ash.Type
41+
def cast_atomic(new_value, constraints) do
42+
if Ash.Expr.expr?(new_value) do
43+
{:atomic, new_value}
44+
else
45+
case cast_input(new_value, constraints) do
46+
{:ok, value} -> {:atomic, value}
47+
{:error, error} -> {:error, error}
48+
end
49+
end
50+
end
51+
52+
@impl Ash.Type
53+
def apply_atomic_constraints(new_value, _constraints) do
54+
{:ok,
55+
Ash.Expr.expr(
56+
if ^new_value in ^@atom_values do
57+
^new_value
58+
else
59+
error(
60+
Ash.Error.Changes.InvalidChanges,
61+
message: "must be one of %{values}",
62+
vars: %{values: ^Enum.join(@atom_values, ", ")}
63+
)
64+
end
65+
)}
66+
end
67+
68+
@impl AshPostgres.Type
69+
def value_to_postgres_default(_, _, value) do
70+
case Map.fetch(@atoms_to_ints, value) do
71+
{:ok, integer} -> {:ok, Integer.to_string(integer)}
72+
:error -> :error
73+
end
74+
end
75+
end

test/type_test.exs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule AshPostgres.Test.TypeTest do
22
use AshPostgres.RepoCase, async: false
33
alias AshPostgres.Test.Post
4+
alias AshPostgres.Test.RSVP
45

56
require Ash.Query
67

@@ -107,4 +108,12 @@ defmodule AshPostgres.Test.TypeTest do
107108
post = Ash.Query.for_read(Post, :with_version_check, version: 1) |> Ash.read!()
108109
refute is_nil(post)
109110
end
111+
112+
test "array expressions work with custom types that map atoms to integers" do
113+
rsvp = RSVP |> Ash.Changeset.for_create(:create, %{response: :accepted}) |> Ash.create!()
114+
115+
updated = rsvp |> Ash.Changeset.for_update(:clear_response, %{}) |> Ash.update!()
116+
117+
assert updated.response == :awaiting
118+
end
110119
end

0 commit comments

Comments
 (0)