improvement: support ash enums

This commit is contained in:
Zach Daniel 2021-04-21 13:49:53 -04:00
parent 8f295e4dc6
commit 301f05604c
15 changed files with 545 additions and 5 deletions

View file

@ -18,7 +18,7 @@ jobs:
matrix:
otp: ["23"]
elixir: ["1.11.0"]
ash: ["master", "1.39.5"]
ash: ["master", "1.41.8"]
pg_version: ["9.5", "9.6", "11"]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

73
lib/migration.ex Normal file
View file

@ -0,0 +1,73 @@
defmodule AshPostgres.Migration do
@moduledoc "Utilities for use in migrations"
@doc """
A utility for creating postgres enums for an Ash enum type.
In your migration, you can say:
```elixir
def up() do
AshPostgres.Migration.create_enum(MyEnumType)
end
```
Attribution:
This code and example was copied from ecto_enum. I didn't use the library itself
because it has a lot that would not currently be relevant for Ash.
https://github.com/gjaldon/ecto_enum
Must be done manually, as the migration generator will not do it.
Additionally, altering the type must be done in its own, separate migration, which
must have `@disable_ddl_transaction true`, as you cannot do this operation
in a transaction.
For example:
```elixir
defmodule MyApp.Repo.Migrations.AddToGenderEnum do
use Ecto.Migration
@disable_ddl_transaction true
def up do
Ecto.Migration.execute "ALTER TYPE gender ADD VALUE IF NOT EXISTS 'other'"
end
def down do
...
end
end
```
Keep in mind, that if you want to create a custom enum type, you will want to add
```elixir
def storage_type, do: :my_type_name
```
"""
def create_enum(type) do
if type.storage_type() == :string do
raise "Must customize the storage_type for #{type} in order to create an enum"
end
types = Enum.map_join(type.values(), ", ", &"'#{&1}'")
Ecto.Migration.execute(
"CREATE TYPE #{type.storage_type()} AS ENUM (#{types})",
"DROP TYPE #{type.storage_type()}"
)
end
def drop_enum(type) do
if type.storage_type() == :string do
raise "Must customize the storage_type for #{type} in order to create an enum"
end
types = Enum.map_join(type.values(), ", ", &"'#{&1}'")
Ecto.Migration.execute(
"DROP TYPE #{type.storage_type()}",
"CREATE TYPE #{type.storage_type()} AS ENUM (#{types})"
)
end
end

View file

@ -95,7 +95,7 @@ defmodule AshPostgres.MixProject do
{:ecto_sql, "~> 3.5"},
{:jason, "~> 1.0"},
{:postgrex, ">= 0.0.0"},
{:ash, ash_version("~> 1.39 and >= 1.39.5")},
{:ash, ash_version("~> 1.41 and >= 1.41.8")},
{:git_ops, "~> 2.0.1", only: :dev},
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
{:ex_check, "~> 0.11.0", only: :dev},

View file

@ -1,5 +1,5 @@
%{
"ash": {:hex, :ash, "1.39.5", "95fc7860f5b15904d64b5bfc18237bde41bb9da68584287155adee613a1065ad", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:timex, ">= 3.0.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "20db450e1bcd8020d1aeef1d4c120ad4d2e23e0f9a92a6b6db8916600bf19b4e"},
"ash": {:hex, :ash, "1.41.8", "91a506b03a3efd75576ac7e20234a5af09f02e3594c2503007c3fc91fe0deca1", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:timex, ">= 3.0.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "7806859710109ba2a96fbcfdf3429b2290af74e3b6d6f2ca04e44bbd3b93559c"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
@ -35,7 +35,7 @@
"postgrex": {:hex, :postgrex, "0.15.7", "724410acd48abac529d0faa6c2a379fb8ae2088e31247687b16cacc0e0883372", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "88310c010ff047cecd73d5ceca1d99205e4b1ab1b9abfdab7e00f5c9d20ef8f9"},
"sobelow": {:hex, :sobelow, "0.10.2", "00e91208046d3b434f9f08779fe0ca7c6d6595b7fa33b289e792dffa6dde8081", [:mix], [], "hexpm", "e30fc994330cf6f485c1c4f2fb7c4b2d403557d0e101c6e5329fd17a58e55a7e"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
"timex": {:hex, :timex, "3.7.5", "3eca56e23bfa4e0848f0b0a29a92fa20af251a975116c6d504966e8a90516dfd", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "a15608dca680f2ef663d71c95842c67f0af08a0f3b1d00e17bbd22872e2874e4"},
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
"tzdata": {:hex, :tzdata, "1.1.0", "72f5babaa9390d0f131465c8702fa76da0919e37ba32baa90d93c583301a8359", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "18f453739b48d3dc5bcf0e8906d2dc112bb40baafe2c707596d89f3c8dd14034"},

View file

@ -0,0 +1,56 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"uuid_generate_v4()\")",
"generated?": false,
"name": "id",
"primary_key?": true,
"references": null,
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "resource_id",
"primary_key?": false,
"references": {
"destination_field": "id",
"destination_field_default": "fragment(\"uuid_generate_v4()\")",
"destination_field_generated": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "comment_ratings_id_fkey",
"on_delete": null,
"on_update": null,
"table": "comments"
},
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "score",
"primary_key?": false,
"references": null,
"type": "bigint"
}
],
"base_filter": null,
"check_constraints": [],
"has_create_action": true,
"hash": "38D98E7F5B0891597BF0D1A1632638954CB7721E01310F989A41D1379E470DF8",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.AshPostgres.TestRepo",
"table": "comment_ratings"
}

View file

@ -0,0 +1,65 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"uuid_generate_v4()\")",
"generated?": false,
"name": "id",
"primary_key?": true,
"references": null,
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "likes",
"primary_key?": false,
"references": null,
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "post_id",
"primary_key?": false,
"references": {
"destination_field": "id",
"destination_field_default": null,
"destination_field_generated": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "special_name_fkey",
"on_delete": "delete",
"on_update": "update",
"table": "posts"
},
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "title",
"primary_key?": false,
"references": null,
"type": "text"
}
],
"base_filter": null,
"check_constraints": [],
"has_create_action": true,
"hash": "AC27C8DBB84D6687136A96A5E1393042E31235B847365E5DB26DE57F5FC55604",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.AshPostgres.TestRepo",
"table": "comments"
}

View file

@ -0,0 +1,56 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"uuid_generate_v4()\")",
"generated?": false,
"name": "id",
"primary_key?": true,
"references": null,
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "resource_id",
"primary_key?": false,
"references": {
"destination_field": "id",
"destination_field_default": "fragment(\"uuid_generate_v4()\")",
"destination_field_generated": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "post_ratings_id_fkey",
"on_delete": null,
"on_update": null,
"table": "posts"
},
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "score",
"primary_key?": false,
"references": null,
"type": "bigint"
}
],
"base_filter": null,
"check_constraints": [],
"has_create_action": true,
"hash": "04586179DB8456574D279F2B6BE7AAEAC78D41E556572970B373607084782402",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.AshPostgres.TestRepo",
"table": "post_ratings"
}

View file

@ -0,0 +1,97 @@
{
"attributes": [
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "category",
"primary_key?": false,
"references": null,
"type": "citext"
},
{
"allow_nil?": false,
"default": "fragment(\"uuid_generate_v4()\")",
"generated?": false,
"name": "id",
"primary_key?": true,
"references": null,
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "price",
"primary_key?": false,
"references": null,
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "public",
"primary_key?": false,
"references": null,
"type": "boolean"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "score",
"primary_key?": false,
"references": null,
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "status",
"primary_key?": false,
"references": null,
"type": "status"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "title",
"primary_key?": false,
"references": null,
"type": "text"
},
{
"allow_nil?": true,
"default": "\"sponsored\"",
"generated?": false,
"name": "type",
"primary_key?": false,
"references": null,
"type": "text"
}
],
"base_filter": "type = 'sponsored'",
"check_constraints": [
{
"attribute": [
"price"
],
"base_filter": "type = 'sponsored'",
"check": "price > 0",
"name": "price_must_be_positive"
}
],
"has_create_action": true,
"hash": "7A770F5F8910A177ACBB48415948238B7A1CA22B0A93D01A3EBF434825D9893C",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.AshPostgres.TestRepo",
"table": "posts"
}

View file

@ -0,0 +1,56 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"uuid_generate_v4()\")",
"generated?": false,
"name": "id",
"primary_key?": true,
"references": null,
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "name",
"primary_key?": false,
"references": null,
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"name": "org_id",
"primary_key?": false,
"references": {
"destination_field": "id",
"destination_field_default": null,
"destination_field_generated": null,
"multitenancy": {
"attribute": "id",
"global": true,
"strategy": "attribute"
},
"name": "multitenant_posts_org_id_fkey",
"on_delete": null,
"on_update": null,
"table": "multitenant_orgs"
},
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"has_create_action": true,
"hash": "799C3F02A636BEE89A64FB570979E01572BBB54FAB744D1F9F86420614A67789",
"identities": [],
"multitenancy": {
"attribute": null,
"global": false,
"strategy": "context"
},
"repo": "Elixir.AshPostgres.TestRepo",
"table": "multitenant_posts"
}

View file

@ -15,6 +15,11 @@ defmodule AshPostgres.TestRepo.Migrations.MigrateResources7 do
create constraint(:posts, :price_must_be_positive, check: "type = 'sponsored' AND price > 0")
# This is just to ensure tests are trying out the enum migration logic
AshPostgres.Migration.create_enum(AshPostgres.Test.Types.Status)
AshPostgres.Migration.drop_enum(AshPostgres.Test.Types.Status)
AshPostgres.Migration.create_enum(AshPostgres.Test.Types.Status)
alter table(:post_ratings) do
modify :score, :bigint
end
@ -56,4 +61,4 @@ defmodule AshPostgres.TestRepo.Migrations.MigrateResources7 do
modify :score, :integer
end
end
end
end

View file

@ -0,0 +1,75 @@
defmodule AshPostgres.TestRepo.Migrations.MigrateResources8 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(:posts) do
add :status, :status
end
drop constraint(:post_ratings, "post_ratings_id_fkey")
drop constraint(:comments, "special_name_fkey")
drop constraint(:comment_ratings, "comment_ratings_id_fkey")
alter table(:comment_ratings) do
modify :resource_id,
references(:comments, column: :id, name: "comment_ratings_id_fkey", type: :uuid)
end
alter table(:comments) do
modify :post_id,
references(:posts,
column: :id,
name: "special_name_fkey",
type: :uuid,
on_delete: :delete_all,
on_update: :update_all
)
end
alter table(:post_ratings) do
modify :resource_id,
references(:posts, column: :id, name: "post_ratings_id_fkey", type: :uuid)
end
end
def down do
drop constraint(:post_ratings, "post_ratings_id_fkey")
alter table(:post_ratings) do
modify :resource_id,
references(:posts, column: :id, name: "post_ratings_id_fkey", type: :uuid)
end
drop constraint(:comments, "special_name_fkey")
alter table(:comments) do
modify :post_id,
references(:posts,
column: :id,
name: "special_name_fkey",
type: :uuid,
on_delete: :delete_all,
on_update: :update_all
)
end
drop constraint(:comment_ratings, "comment_ratings_id_fkey")
alter table(:comment_ratings) do
modify :resource_id,
references(:comments, column: :id, name: "comment_ratings_id_fkey", type: :uuid)
end
alter table(:posts) do
remove :status
end
end
end

View file

@ -0,0 +1,37 @@
defmodule AshPostgres.TestRepo.TenantMigrations.MigrateResources4 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
drop constraint(:multitenant_posts, "multitenant_posts_org_id_fkey")
alter table(:multitenant_posts, prefix: prefix()) do
modify :org_id,
references(:multitenant_orgs,
column: :id,
prefix: "public",
name: "multitenant_posts_org_id_fkey",
type: :uuid
)
end
end
def down do
drop constraint(:multitenant_posts, "multitenant_posts_org_id_fkey")
alter table(:multitenant_posts, prefix: prefix()) do
modify :org_id,
references(:multitenant_orgs,
column: :id,
prefix: "public",
name: "multitenant_posts_org_id_fkey",
type: :uuid
)
end
end
end

13
test/enum_test.exs Normal file
View file

@ -0,0 +1,13 @@
defmodule AshPostgres.EnumTest do
@moduledoc false
use AshPostgres.RepoCase, async: false
alias AshPostgres.Test.{Api, Post}
require Ash.Query
test "valid values are properly inserted" do
Post
|> Ash.Changeset.new(%{title: "title", status: :open})
|> Api.create!()
end
end

View file

@ -44,6 +44,7 @@ defmodule AshPostgres.Test.Post do
attribute(:category, :ci_string)
attribute(:type, :atom, default: :sponsored, private?: true, writable?: false)
attribute(:price, :integer)
attribute(:status, AshPostgres.Test.Types.Status)
end
relationships do

View file

@ -0,0 +1,6 @@
defmodule AshPostgres.Test.Types.Status do
@moduledoc false
use Ash.Type.Enum, values: [:open, :closed]
def storage_type, do: :status
end