diff --git a/.check.exs b/.check.exs index 3e8ac70..477a94e 100644 --- a/.check.exs +++ b/.check.exs @@ -11,7 +11,8 @@ ## ...or adjusted (e.g. use one-line formatter for more compact credo output) # {:credo, "mix credo --format oneline"}, - {:check_formatter, command: "mix ash.formatter --check"} + {:check_formatter, command: "mix ash.formatter --check"}, + {:check_migrations, command: "mix test.check_migrations"} ## custom new tools may be added (mix tasks or arbitrary commands) # {:my_mix_task, command: "mix release", env: %{"MIX_ENV" => "prod"}}, # {:my_arbitrary_tool, command: "npm test", cd: "assets"}, diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 13e5b20..4588a32 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -19,7 +19,7 @@ jobs: otp: ["23"] elixir: ["1.13.2"] ash: ["main", "1.52.0-rc.1"] - pg_version: ["9.6", "11"] + pg_version: ["9.6", "11", "12", "13"] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ASH_VERSION: ${{matrix.ash}} diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 3b59df6..c2aab92 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -22,7 +22,7 @@ defmodule AshPostgres.MigrationGenerator do no_shell?: false, format: true, dry_run: false, - check_generated: false, + check: false, drop_columns: false def generate(apis, opts \\ []) do @@ -97,7 +97,7 @@ defmodule AshPostgres.MigrationGenerator do defp opts(opts) do case struct(__MODULE__, opts) do - %{check_generated: true} = opts -> + %{check: true} = opts -> %{opts | dry_run: true} opts -> @@ -214,7 +214,16 @@ defmodule AshPostgres.MigrationGenerator do :ok operations -> - if opts.check_generated, do: exit({:shutdown, 1}) + if opts.check do + IO.puts(""" + Migrations would have been generated, but the --check flag was provided. + + To see what migration would have been generated, run with the `--dry-run` + option instead. To generate those migrations, run without either flag. + """) + + exit({:shutdown, 1}) + end operations |> organize_operations @@ -1695,7 +1704,7 @@ defmodule AshPostgres.MigrationGenerator do destination_field_generated: source_attribute.generated?, multitenancy: multitenancy(relationship.source), table: AshPostgres.table(relationship.source), - schema: AshPostgres.table(relationship.source), + schema: AshPostgres.schema(relationship.source), on_delete: AshPostgres.polymorphic_on_delete(relationship.source), on_update: AshPostgres.polymorphic_on_update(relationship.source), name: diff --git a/lib/mix/tasks/ash_postgres.generate_migrations.ex b/lib/mix/tasks/ash_postgres.generate_migrations.ex index 3ff7b10..fcd8f5b 100644 --- a/lib/mix/tasks/ash_postgres.generate_migrations.ex +++ b/lib/mix/tasks/ash_postgres.generate_migrations.ex @@ -20,7 +20,7 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do * `quiet` - messages for file creations will not be printed * `no-format` - files that are created will not be formatted with the code formatter * `dry-run` - no files are created, instead the new migration is printed - * `check-migrated` - no files are created, returns an exit(1) code if the current snapshots and resources don't fit + * `check` - no files are created, returns an exit(1) code if the current snapshots and resources don't fit #### Snapshots @@ -83,7 +83,7 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do name: :string, no_format: :boolean, dry_run: :boolean, - check_migrated: :boolean, + check: :boolean, drop_columns: :boolean ] ) diff --git a/mix.exs b/mix.exs index c692190..38a91de 100644 --- a/mix.exs +++ b/mix.exs @@ -24,6 +24,7 @@ defmodule AshPostgres.MixProject do "test.create": :test, "test.migrate": :test, "test.migrate_tenants": :test, + "test.check_migrations": :test, "test.drop": :test, "test.generate_migrations": :test, "test.reset": :test @@ -124,6 +125,7 @@ defmodule AshPostgres.MixProject do credo: "credo --strict", "ash.formatter": "ash.formatter --extensions AshPostgres.DataLayer", "test.generate_migrations": "ash_postgres.generate_migrations", + "test.check_migrations": "ash_postgres.generate_migrations --check", "test.migrate_tenants": "ash_postgres.migrate --tenants", "test.migrate": "ash_postgres.migrate", "test.create": "ash_postgres.create", diff --git a/priv/resource_snapshots/test_repo/comments/20220518160829.json b/priv/resource_snapshots/test_repo/comments/20220518160829.json new file mode 100644 index 0000000..5913134 --- /dev/null +++ b/priv/resource_snapshots/test_repo/comments/20220518160829.json @@ -0,0 +1,116 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v4()\")", + "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": "title", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "likes", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "arbitrary_timestamp", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"now()\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "created_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "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", + "schema": "public", + "table": "posts" + }, + "size": null, + "source": "post_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "destination_field": "id", + "destination_field_default": null, + "destination_field_generated": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "comments_author_id_fkey", + "on_delete": null, + "on_update": null, + "schema": "public", + "table": "authors" + }, + "size": null, + "source": "author_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "has_create_action": true, + "hash": "6EBA91B3BBCDE1477F386C62CEABB28AB9FE9B8E688CB849088145374DC1EB85", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "comments" +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/post_links/20220518160829.json b/priv/resource_snapshots/test_repo/post_links/20220518160829.json new file mode 100644 index 0000000..dcd2c1a --- /dev/null +++ b/priv/resource_snapshots/test_repo/post_links/20220518160829.json @@ -0,0 +1,66 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": true, + "references": { + "destination_field": "id", + "destination_field_default": null, + "destination_field_generated": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "post_links_source_post_id_fkey", + "on_delete": null, + "on_update": null, + "schema": "public", + "table": "posts" + }, + "size": null, + "source": "source_post_id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": true, + "references": { + "destination_field": "id", + "destination_field_default": null, + "destination_field_generated": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "post_links_destination_post_id_fkey", + "on_delete": null, + "on_update": null, + "schema": "public", + "table": "posts" + }, + "size": null, + "source": "destination_post_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "has_create_action": true, + "hash": "ABE3281B7C35040EA3BE51CA5475CA5AFD2840307E3541BE9B37D316A1F7898D", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "post_links" +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/posts/20220518160829.json b/priv/resource_snapshots/test_repo/posts/20220518160829.json new file mode 100644 index 0000000..1796110 --- /dev/null +++ b/priv/resource_snapshots/test_repo/posts/20220518160829.json @@ -0,0 +1,150 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v4()\")", + "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": "title", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "score", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "public", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "category", + "type": "citext" + }, + { + "allow_nil?": true, + "default": "\"sponsored\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "type", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "price", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "\"0\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "decimal", + "type": "decimal" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "status", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "status_enum", + "type": "status" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "point", + "type": [ + "array", + "float" + ] + }, + { + "allow_nil?": false, + "default": "fragment(\"now()\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "created_at", + "type": "utc_datetime_usec" + } + ], + "base_filter": "type = 'sponsored'", + "check_constraints": [ + { + "attribute": [ + "price" + ], + "base_filter": "type = 'sponsored'", + "check": "price > 0", + "name": "price_must_be_positive" + } + ], + "custom_indexes": [], + "has_create_action": true, + "hash": "9D6F943BF87E7AA79511EA5102F07D10972DF830089F0C10785C7518BA11943A", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "posts" +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/tenants/multitenant_posts/20220518160827.json b/priv/resource_snapshots/test_repo/tenants/multitenant_posts/20220518160827.json new file mode 100644 index 0000000..4527f29 --- /dev/null +++ b/priv/resource_snapshots/test_repo/tenants/multitenant_posts/20220518160827.json @@ -0,0 +1,86 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v4()\")", + "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": { + "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, + "schema": "public", + "table": "multitenant_orgs" + }, + "size": null, + "source": "org_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "destination_field": "id", + "destination_field_default": null, + "destination_field_generated": null, + "multitenancy": { + "attribute": "org_id", + "global": true, + "strategy": "attribute" + }, + "name": "multitenant_posts_user_id_fkey", + "on_delete": null, + "on_update": null, + "schema": "public", + "table": "users" + }, + "size": null, + "source": "user_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "has_create_action": true, + "hash": "B2BB130AF001CA540847811EDBDCE8E5A5C2B050EB1B951556338882E7B0619A", + "identities": [], + "multitenancy": { + "attribute": null, + "global": false, + "strategy": "context" + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "multitenant_posts" +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/users/20220518160829.json b/priv/resource_snapshots/test_repo/users/20220518160829.json new file mode 100644 index 0000000..fd1cb93 --- /dev/null +++ b/priv/resource_snapshots/test_repo/users/20220518160829.json @@ -0,0 +1,62 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v4()\")", + "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": { + "destination_field": "id", + "destination_field_default": null, + "destination_field_generated": null, + "multitenancy": { + "attribute": "id", + "global": true, + "strategy": "attribute" + }, + "name": "users_org_id_fkey", + "on_delete": null, + "on_update": null, + "schema": "public", + "table": "multitenant_orgs" + }, + "size": null, + "source": "org_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "has_create_action": true, + "hash": "B8098B00555AB33A5833083A543A54D937B3C4C76AFE35507729D823BB18A5E2", + "identities": [], + "multitenancy": { + "attribute": "org_id", + "global": true, + "strategy": "attribute" + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "users" +} \ No newline at end of file diff --git a/priv/test_repo/migrations/20220518160829_migrate_resources4.exs b/priv/test_repo/migrations/20220518160829_migrate_resources4.exs new file mode 100644 index 0000000..5ca0ef9 --- /dev/null +++ b/priv/test_repo/migrations/20220518160829_migrate_resources4.exs @@ -0,0 +1,130 @@ +defmodule AshPostgres.TestRepo.Migrations.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(:users, "users_org_id_fkey") + + alter table(:users) do + modify :org_id, + references(:multitenant_orgs, + column: :id, + name: "users_org_id_fkey", + type: :uuid, + prefix: "public" + ) + end + + alter table(:posts) do + modify :decimal, :decimal, default: "0" + add :point, {:array, :float} + end + + drop constraint(:post_links, "post_links_destination_post_id_fkey") + + drop constraint(:post_links, "post_links_source_post_id_fkey") + + alter table(:post_links) do + modify :source_post_id, + references(:posts, + column: :id, + prefix: "public", + name: "post_links_source_post_id_fkey", + type: :uuid + ) + end + + alter table(:post_links) do + modify :destination_post_id, + references(:posts, + column: :id, + prefix: "public", + name: "post_links_destination_post_id_fkey", + type: :uuid + ) + end + + drop constraint(:comments, "comments_author_id_fkey") + + drop constraint(:comments, "special_name_fkey") + + alter table(:comments) do + modify :post_id, + references(:posts, + column: :id, + prefix: "public", + name: "special_name_fkey", + type: :uuid, + on_delete: :delete_all, + on_update: :update_all + ) + end + + alter table(:comments) do + modify :author_id, + references(:authors, + column: :id, + prefix: "public", + name: "comments_author_id_fkey", + type: :uuid + ) + end + end + + def down do + drop constraint(:comments, "comments_author_id_fkey") + + alter table(:comments) do + modify :author_id, + references(:authors, column: :id, name: "comments_author_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(:post_links, "post_links_destination_post_id_fkey") + + alter table(:post_links) do + modify :destination_post_id, + references(:posts, + column: :id, + name: "post_links_destination_post_id_fkey", + type: :uuid + ) + end + + drop constraint(:post_links, "post_links_source_post_id_fkey") + + alter table(:post_links) do + modify :source_post_id, + references(:posts, column: :id, name: "post_links_source_post_id_fkey", type: :uuid) + end + + alter table(:posts) do + remove :point + modify :decimal, :decimal, default: 0 + end + + drop constraint(:users, "users_org_id_fkey") + + alter table(:users) do + modify :org_id, + references(:multitenant_orgs, column: :id, name: "users_org_id_fkey", type: :uuid) + end + end +end \ No newline at end of file diff --git a/priv/test_repo/tenant_migrations/20220518160827_migrate_resources2.exs b/priv/test_repo/tenant_migrations/20220518160827_migrate_resources2.exs new file mode 100644 index 0000000..1e586ac --- /dev/null +++ b/priv/test_repo/tenant_migrations/20220518160827_migrate_resources2.exs @@ -0,0 +1,55 @@ +defmodule AshPostgres.TestRepo.TenantMigrations.MigrateResources2 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_user_id_fkey") + + 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 + + alter table(:multitenant_posts, prefix: prefix()) do + modify :user_id, + references(:users, + column: :id, + prefix: "public", + name: "multitenant_posts_user_id_fkey", + type: :uuid + ) + end + end + + def down do + drop constraint(:multitenant_posts, "multitenant_posts_user_id_fkey") + + alter table(:multitenant_posts, prefix: prefix()) do + modify :user_id, + references(:users, column: :id, name: "multitenant_posts_user_id_fkey", type: :uuid) + end + + drop constraint(:multitenant_posts, "multitenant_posts_org_id_fkey") + + alter table(:multitenant_posts, prefix: prefix()) do + modify :org_id, + references(:multitenant_orgs, + column: :id, + name: "multitenant_posts_org_id_fkey", + type: :uuid + ) + end + end +end \ No newline at end of file diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index d805119..c28ab7f 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -539,7 +539,7 @@ defmodule AshPostgres.MigrationGeneratorTest do end end - describe "--check_migrated option" do + describe "--check option" do setup do defposts do attributes do @@ -558,7 +558,7 @@ defmodule AshPostgres.MigrationGeneratorTest do AshPostgres.MigrationGenerator.generate(api, snapshot_path: "test_snapshot_path", migration_path: "test_migration_path", - check_generated: true + check: true ) ) == {:shutdown, 1} diff --git a/test/support/resources/post.ex b/test/support/resources/post.ex index ef512ac..a172de1 100644 --- a/test/support/resources/post.ex +++ b/test/support/resources/post.ex @@ -57,6 +57,7 @@ defmodule AshPostgres.Test.Post do attribute(:status, AshPostgres.Test.Types.Status) attribute(:status_enum, AshPostgres.Test.Types.StatusEnum) attribute(:status_enum_no_cast, AshPostgres.Test.Types.StatusEnumNoCast, source: :status_enum) + attribute(:point, AshPostgres.Test.Point) create_timestamp(:created_at) end diff --git a/test/support/types/point.ex b/test/support/types/point.ex new file mode 100644 index 0000000..b22d36f --- /dev/null +++ b/test/support/types/point.ex @@ -0,0 +1,34 @@ +defmodule AshPostgres.Test.Point do + @moduledoc false + use Ash.Type + + def storage_type, do: {:array, :float} + + def cast_input(nil, _), do: {:ok, nil} + + def cast_input({a, b, c}, _) when is_float(a) and is_float(b) and is_float(c) do + {:ok, {a, b, c}} + end + + def cast_input(_, _), do: :error + + def cast_stored(nil, _), do: {:ok, nil} + + def cast_stored([a, b, c], _) when is_float(a) and is_float(b) and is_float(c) do + {:ok, {a, b, c}} + end + + def cast_stored(_, _) do + :error + end + + def dump_to_native(nil, _), do: {:ok, nil} + + def dump_to_native({a, b, c}, _) when is_float(a) and is_float(b) and is_float(c) do + {:ok, [a, b, c]} + end + + def dump_to_native(_, _) do + :error + end +end diff --git a/test/type_test.exs b/test/type_test.exs new file mode 100644 index 0000000..97ffac1 --- /dev/null +++ b/test/type_test.exs @@ -0,0 +1,30 @@ +defmodule AshPostgres.Test.TypeTest do + use AshPostgres.RepoCase, async: false + alias AshPostgres.Test.{Api, Post} + + require Ash.Query + + test "complex custom types can be used" do + post = + Post + |> Ash.Changeset.new(%{title: "title", point: {1.0, 2.0, 3.0}}) + |> Api.create!() + + assert post.point == {1.0, 2.0, 3.0} + end + + test "complex custom types can be accessed with fragments" do + Post + |> Ash.Changeset.new(%{title: "title", point: {1.0, 2.0, 3.0}}) + |> Api.create!() + + Post + |> Ash.Changeset.new(%{title: "title", point: {2.0, 1.0, 3.0}}) + |> Api.create!() + + assert [%{point: {2.0, 1.0, 3.0}}] = + Post + |> Ash.Query.filter(fragment("(?)[1] > (?)[2]", point, point)) + |> Api.read!() + end +end