Skip to content

Commit 64d768c

Browse files
authored
improvement: Add support for field names in idenitity constraints (#478)
1 parent c2b2a7d commit 64d768c

File tree

6 files changed

+138
-13
lines changed

6 files changed

+138
-13
lines changed

lib/data_layer.ex

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2310,7 +2310,7 @@ defmodule AshPostgres.DataLayer do
23102310
%Postgrex.Error{} = error,
23112311
stacktrace,
23122312
{:bulk_create, fake_changeset},
2313-
_resource
2313+
resource
23142314
) do
23152315
case Ecto.Adapters.Postgres.Connection.to_constraints(error, []) do
23162316
[] ->
@@ -2319,7 +2319,7 @@ defmodule AshPostgres.DataLayer do
23192319
constraints ->
23202320
{:error,
23212321
fake_changeset
2322-
|> constraints_to_errors(:insert, constraints)
2322+
|> constraints_to_errors(:insert, constraints, resource)
23232323
|> Ash.Error.to_ash_error()}
23242324
end
23252325
end
@@ -2372,7 +2372,7 @@ defmodule AshPostgres.DataLayer do
23722372
{:error, Ash.Error.to_ash_error(error, stacktrace)}
23732373
end
23742374

2375-
defp constraints_to_errors(%{constraints: user_constraints} = changeset, action, constraints) do
2375+
defp constraints_to_errors(%{constraints: user_constraints} = changeset, action, constraints, resource) do
23762376
Enum.map(constraints, fn {type, constraint} ->
23772377
user_constraint =
23782378
Enum.find(user_constraints, fn c ->
@@ -2387,14 +2387,25 @@ defmodule AshPostgres.DataLayer do
23872387

23882388
case user_constraint do
23892389
%{field: field, error_message: error_message, type: type, constraint: constraint} ->
2390-
Ash.Error.Changes.InvalidAttribute.exception(
2391-
field: field,
2392-
message: error_message,
2393-
private_vars: [
2394-
constraint: constraint,
2395-
constraint_type: type
2396-
]
2397-
)
2390+
identities = Ash.Resource.Info.identities(resource)
2391+
table = AshPostgres.DataLayer.Info.table(resource)
2392+
2393+
identity = Enum.find(identities, fn identity ->
2394+
"#{table}_#{identity.name}_index" == constraint
2395+
end)
2396+
2397+
field_names = if identity, do: identity.field_names, else: [field]
2398+
2399+
Enum.map(field_names, fn field_name ->
2400+
Ash.Error.Changes.InvalidAttribute.exception(
2401+
field: field_name,
2402+
message: error_message,
2403+
private_vars: [
2404+
constraint: constraint,
2405+
constraint_type: type
2406+
]
2407+
)
2408+
end)
23982409

23992410
nil ->
24002411
Ecto.ConstraintError.exception(

mix.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ defmodule AshPostgres.MixProject do
165165
# Run "mix help deps" to learn about dependencies.
166166
defp deps do
167167
[
168-
{:ash, ash_version("~> 3.4 and >= 3.4.48")},
168+
{:ash, ash_version("~> 3.4 and >= 3.4.64")},
169169
{:ash_sql, ash_sql_version("~> 0.2 and >= 0.2.43")},
170170
{:igniter, "~> 0.5 and >= 0.5.16", optional: true},
171171
{:ecto_sql, "~> 3.12"},
@@ -197,7 +197,7 @@ defmodule AshPostgres.MixProject do
197197
[path: "../ash", override: true]
198198

199199
"main" ->
200-
[git: "https://github.com/ash-project/ash.git"]
200+
[git: "https://github.com/ash-project/ash.git", override: true]
201201

202202
version when is_binary(version) ->
203203
"~> #{version}"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"attributes": [
3+
{
4+
"allow_nil?": false,
5+
"default": "fragment(\"gen_random_uuid()\")",
6+
"generated?": false,
7+
"primary_key?": true,
8+
"references": null,
9+
"size": null,
10+
"source": "id",
11+
"type": "uuid"
12+
},
13+
{
14+
"allow_nil?": true,
15+
"default": "nil",
16+
"generated?": false,
17+
"primary_key?": false,
18+
"references": null,
19+
"size": null,
20+
"source": "name",
21+
"type": "text"
22+
},
23+
{
24+
"allow_nil?": true,
25+
"default": "nil",
26+
"generated?": false,
27+
"primary_key?": false,
28+
"references": null,
29+
"size": null,
30+
"source": "department",
31+
"type": "text"
32+
}
33+
],
34+
"base_filter": null,
35+
"check_constraints": [],
36+
"custom_indexes": [],
37+
"custom_statements": [],
38+
"has_create_action": true,
39+
"hash": "1D1BA9E1E272238D80C9861CAA67C4A85F675E3B052A15F4D5AC272551B820A7",
40+
"identities": [
41+
{
42+
"all_tenants?": false,
43+
"base_filter": null,
44+
"index_name": "orgs_department_index",
45+
"keys": [
46+
{
47+
"type": "string",
48+
"value": "(LOWER(department))"
49+
}
50+
],
51+
"name": "department",
52+
"nils_distinct?": true,
53+
"where": null
54+
}
55+
],
56+
"multitenancy": {
57+
"attribute": null,
58+
"global": null,
59+
"strategy": null
60+
},
61+
"repo": "Elixir.AshPostgres.TestRepo",
62+
"schema": null,
63+
"table": "orgs"
64+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
defmodule AshPostgres.TestRepo.Migrations.MigrateResources49 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+
alter table(:orgs) do
12+
add(:department, :text)
13+
end
14+
15+
create(unique_index(:orgs, ["(LOWER(department))"], name: "orgs_department_index"))
16+
end
17+
18+
def down do
19+
drop_if_exists(unique_index(:orgs, ["(LOWER(department))"], name: "orgs_department_index"))
20+
21+
alter table(:orgs) do
22+
remove(:department)
23+
end
24+
end
25+
end

test/support/resources/organization.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ defmodule AshPostgres.Test.Organization do
1010
postgres do
1111
table("orgs")
1212
repo(AshPostgres.TestRepo)
13+
14+
calculations_to_sql(lower_department: "LOWER(department)")
1315
end
1416

1517
policies do
@@ -39,6 +41,15 @@ defmodule AshPostgres.Test.Organization do
3941
attributes do
4042
uuid_primary_key(:id, writable?: true)
4143
attribute(:name, :string, public?: true)
44+
attribute(:department, :string, public?: true)
45+
end
46+
47+
calculations do
48+
calculate(:lower_department, :string, expr(fragment("LOWER(?)", department)))
49+
end
50+
51+
identities do
52+
identity(:department, [:lower_department], field_names: [:department_slug])
4253
end
4354

4455
relationships do

test/unique_identity_test.exs

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

56
require Ash.Query
67

@@ -19,6 +20,19 @@ defmodule AshPostgres.Test.UniqueIdentityTest do
1920
end
2021
end
2122

23+
test "unique constraint field names are property set" do
24+
Organization
25+
|> Ash.Changeset.for_create(:create, %{name: "Acme", department: "Sales"})
26+
|> Ash.create!()
27+
28+
assert {:error, %Ash.Error.Invalid{errors: [invalid_attribute]}} =
29+
Organization
30+
|> Ash.Changeset.for_create(:create, %{name: "Acme", department: "SALES"})
31+
|> Ash.create()
32+
33+
assert %Ash.Error.Changes.InvalidAttribute{field: :department_slug} = invalid_attribute
34+
end
35+
2236
test "a unique constraint can be used to upsert when the resource has a base filter" do
2337
post =
2438
Post

0 commit comments

Comments
 (0)