Skip to content

Commit

Permalink
improvement: Add support for field names in idenitity constraints (#478)
Browse files Browse the repository at this point in the history
  • Loading branch information
lcmen authored Feb 17, 2025
1 parent c2b2a7d commit 64d768c
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 13 deletions.
33 changes: 22 additions & 11 deletions lib/data_layer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2310,7 +2310,7 @@ defmodule AshPostgres.DataLayer do
%Postgrex.Error{} = error,
stacktrace,
{:bulk_create, fake_changeset},
_resource
resource
) do
case Ecto.Adapters.Postgres.Connection.to_constraints(error, []) do
[] ->
Expand All @@ -2319,7 +2319,7 @@ defmodule AshPostgres.DataLayer do
constraints ->
{:error,
fake_changeset
|> constraints_to_errors(:insert, constraints)
|> constraints_to_errors(:insert, constraints, resource)
|> Ash.Error.to_ash_error()}
end
end
Expand Down Expand Up @@ -2372,7 +2372,7 @@ defmodule AshPostgres.DataLayer do
{:error, Ash.Error.to_ash_error(error, stacktrace)}
end

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

case user_constraint do
%{field: field, error_message: error_message, type: type, constraint: constraint} ->
Ash.Error.Changes.InvalidAttribute.exception(
field: field,
message: error_message,
private_vars: [
constraint: constraint,
constraint_type: type
]
)
identities = Ash.Resource.Info.identities(resource)
table = AshPostgres.DataLayer.Info.table(resource)

identity = Enum.find(identities, fn identity ->
"#{table}_#{identity.name}_index" == constraint
end)

field_names = if identity, do: identity.field_names, else: [field]

Enum.map(field_names, fn field_name ->
Ash.Error.Changes.InvalidAttribute.exception(
field: field_name,
message: error_message,
private_vars: [
constraint: constraint,
constraint_type: type
]
)
end)

nil ->
Ecto.ConstraintError.exception(
Expand Down
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ defmodule AshPostgres.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ash, ash_version("~> 3.4 and >= 3.4.48")},
{:ash, ash_version("~> 3.4 and >= 3.4.64")},
{:ash_sql, ash_sql_version("~> 0.2 and >= 0.2.43")},
{:igniter, "~> 0.5 and >= 0.5.16", optional: true},
{:ecto_sql, "~> 3.12"},
Expand Down Expand Up @@ -197,7 +197,7 @@ defmodule AshPostgres.MixProject do
[path: "../ash", override: true]

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

version when is_binary(version) ->
"~> #{version}"
Expand Down
64 changes: 64 additions & 0 deletions priv/resource_snapshots/test_repo/orgs/20250210191116.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"primary_key?": true,
"references": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "department",
"type": "text"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "1D1BA9E1E272238D80C9861CAA67C4A85F675E3B052A15F4D5AC272551B820A7",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "orgs_department_index",
"keys": [
{
"type": "string",
"value": "(LOWER(department))"
}
],
"name": "department",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.AshPostgres.TestRepo",
"schema": null,
"table": "orgs"
}
25 changes: 25 additions & 0 deletions priv/test_repo/migrations/20250210191116_migrate_resources49.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule AshPostgres.TestRepo.Migrations.MigrateResources49 do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""

use Ecto.Migration

def up do
alter table(:orgs) do
add(:department, :text)
end

create(unique_index(:orgs, ["(LOWER(department))"], name: "orgs_department_index"))
end

def down do
drop_if_exists(unique_index(:orgs, ["(LOWER(department))"], name: "orgs_department_index"))

alter table(:orgs) do
remove(:department)
end
end
end
11 changes: 11 additions & 0 deletions test/support/resources/organization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ defmodule AshPostgres.Test.Organization do
postgres do
table("orgs")
repo(AshPostgres.TestRepo)

calculations_to_sql(lower_department: "LOWER(department)")
end

policies do
Expand Down Expand Up @@ -39,6 +41,15 @@ defmodule AshPostgres.Test.Organization do
attributes do
uuid_primary_key(:id, writable?: true)
attribute(:name, :string, public?: true)
attribute(:department, :string, public?: true)
end

calculations do
calculate(:lower_department, :string, expr(fragment("LOWER(?)", department)))
end

identities do
identity(:department, [:lower_department], field_names: [:department_slug])
end

relationships do
Expand Down
14 changes: 14 additions & 0 deletions test/unique_identity_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule AshPostgres.Test.UniqueIdentityTest do
use AshPostgres.RepoCase, async: false
alias AshPostgres.Test.Post
alias AshPostgres.Test.Organization

require Ash.Query

Expand All @@ -19,6 +20,19 @@ defmodule AshPostgres.Test.UniqueIdentityTest do
end
end

test "unique constraint field names are property set" do
Organization
|> Ash.Changeset.for_create(:create, %{name: "Acme", department: "Sales"})
|> Ash.create!()

assert {:error, %Ash.Error.Invalid{errors: [invalid_attribute]}} =
Organization
|> Ash.Changeset.for_create(:create, %{name: "Acme", department: "SALES"})
|> Ash.create()

assert %Ash.Error.Changes.InvalidAttribute{field: :department_slug} = invalid_attribute
end

test "a unique constraint can be used to upsert when the resource has a base filter" do
post =
Post
Expand Down

0 comments on commit 64d768c

Please sign in to comment.