From e2a0e1cabd3d6445c0faa2e8c2227b5e84d94820 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Thu, 5 Sep 2024 15:03:20 -0400 Subject: [PATCH] test: more tests for archival, including postgres tests --- .github/workflows/elixir.yml | 3 + config/config.exs | 11 ++ .../dsls/DSL:-AshArchival.Resource.md | 1 + mix.exs | 9 +- mix.lock | 7 +- .../test_repo/extensions.json | 6 + .../test_repo/posts/20240905173414.json | 39 +++++ ...3232_install_ash-functions_extension_4.exs | 139 ++++++++++++++++++ .../migrations/20240905173414_add_posts.exs | 20 +++ test/archival_test.exs | 49 ++++++ test/postgres_test.exs | 35 +++++ test/support/domain.ex | 8 + test/support/post.ex | 33 +++++ test/support/repo.ex | 20 +++ test/support/repo_case.ex | 28 ++++ test/test_helper.exs | 2 + 16 files changed, 407 insertions(+), 3 deletions(-) create mode 100644 priv/resource_snapshots/test_repo/extensions.json create mode 100644 priv/resource_snapshots/test_repo/posts/20240905173414.json create mode 100644 priv/test_repo/migrations/20240905173232_install_ash-functions_extension_4.exs create mode 100644 priv/test_repo/migrations/20240905173414_add_posts.exs create mode 100644 test/postgres_test.exs create mode 100644 test/support/domain.ex create mode 100644 test/support/post.ex create mode 100644 test/support/repo.ex create mode 100644 test/support/repo_case.ex diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 332ea5d..f81da53 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -9,3 +9,6 @@ on: jobs: ash-ci: uses: ash-project/ash/.github/workflows/ash-ci.yml@main + with: + postgres: true + postgres-version: "16" diff --git a/config/config.exs b/config/config.exs index 770a4ae..a9afd5b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,9 +1,20 @@ import Config if Mix.env() == :test do + config :ash_archival, ash_domains: [AshArchival.Test.Domain] + + config :ash_archival, + ecto_repos: [AshArchival.TestRepo] + config :ash, :validate_domain_resource_inclusion?, false config :ash, :validate_domain_config_inclusion?, false config :logger, level: :warning + + config :ash_archival, AshArchival.TestRepo, + username: "postgres", + database: "ash_archival_test", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox end if Mix.env() == :dev do diff --git a/documentation/dsls/DSL:-AshArchival.Resource.md b/documentation/dsls/DSL:-AshArchival.Resource.md index ca7cd3c..8faf542 100644 --- a/documentation/dsls/DSL:-AshArchival.Resource.md +++ b/documentation/dsls/DSL:-AshArchival.Resource.md @@ -21,6 +21,7 @@ A section for configuring how archival is configured for a resource. | Name | Type | Default | Docs | |------|------|---------|------| | [`attribute`](#archive-attribute){: #archive-attribute } | `atom` | `:archived_at` | The attribute in which to store the archival flag (the current datetime). | +| [`attribute_type`](#archive-attribute_type){: #archive-attribute_type } | `atom` | `:utc_datetime_usec` | The attribute type. | | [`base_filter?`](#archive-base_filter?){: #archive-base_filter? } | `atom` | `false` | Whether or not a base filter exists that applies the `is_nil(archived_at)` rule. | | [`exclude_read_actions`](#archive-exclude_read_actions){: #archive-exclude_read_actions } | `atom \| list(atom)` | `[]` | A read action or actions that should show archived items. They will not get the automatic `is_nil(archived_at)` filter. | | [`exclude_upsert_actions`](#archive-exclude_upsert_actions){: #archive-exclude_upsert_actions } | `atom \| list(atom)` | `[]` | This option is deprecated as it no longer has any effect. Upserts are handled according to the upsert identity. See the upserts guide for more. | diff --git a/mix.exs b/mix.exs index 2e732f9..e478182 100644 --- a/mix.exs +++ b/mix.exs @@ -13,6 +13,7 @@ defmodule AshArchival.MixProject do elixir: "~> 1.13", source_url: "https://github.com/ash-project/ash_archival", homepage_url: "https://github.com/ash-project/ash_archival", + elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, description: @description, aliases: aliases(), @@ -23,6 +24,9 @@ defmodule AshArchival.MixProject do ] end + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + defp package do [ name: :ash_archival, @@ -93,6 +97,7 @@ defmodule AshArchival.MixProject do [ {:ash, ash_version("~> 3.0 and >= 3.0.5")}, # dev/test dependencies + {:ash_postgres, "~> 2.3", only: [:dev, :test]}, {:simple_sat, "~> 0.1.0", only: [:dev, :test]}, {:git_ops, "~> 2.5", only: [:dev, :test]}, {:ex_doc, github: "elixir-lang/ex_doc", only: [:dev, :test], runtime: false}, @@ -125,8 +130,8 @@ defmodule AshArchival.MixProject do defp ash_version(default_version) do case System.get_env("ASH_VERSION") do nil -> default_version - "local" -> [path: "../ash"] - "main" -> [git: "https://github.com/ash-project/ash.git"] + "local" -> [path: "../ash", override: true] + "main" -> [git: "https://github.com/ash-project/ash.git", override: true] version -> "~> #{version}" end end diff --git a/mix.lock b/mix.lock index 4ad6b19..33e7b21 100644 --- a/mix.lock +++ b/mix.lock @@ -1,11 +1,15 @@ %{ - "ash": {:hex, :ash, "3.4.1", "14bfccd4c1e7c17db5aed1ecb5062875f55b56b67f6fba911f3a8ef6739f3cfd", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.11 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.9", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.8 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1e3127e0af0698e652a6bbfb4d4f1a3bb8a48fb42833f4e8f00eda8f1a93082b"}, + "ash": {:hex, :ash, "3.4.5", "ea5cb8f2ce11c4610cb8874c3d7e8952a822759d34bc01c6dd5c8ec6174b9146", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.11 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.9", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.22 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cecc2a3a3e5c9998fd5415eb53aaf8511ad5acc4e71cc8bdbf1ec5acb6383faa"}, + "ash_postgres": {:hex, :ash_postgres, "2.3.1", "23eaa95063a25d1332845089dbb0ae64e01ef4ed413abfc9168738e9b7f23f54", [:mix], [{:ash, ">= 3.4.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.30 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, ">= 3.12.1 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:inflex, "~> 2.1", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "886659fe8e5013f2b4c2382a5dc71b3f06b566f663a39e6310d7d116db0bdace"}, + "ash_sql": {:hex, :ash_sql, "0.2.32", "de99255becfb9daa7991c18c870e9f276bb372acda7eda3e05c3e2ff2ca8922e", [:mix], [{:ash, ">= 3.1.7 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "43773bcd33d21319c11804d76fe11f1a1b7c8faba7aaedeab6f55fde3d2405db"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "ecto": {:hex, :ecto, "3.12.2", "bae2094f038e9664ce5f089e5f3b6132a535d8b018bd280a485c2f33df5c0ce1", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "492e67c70f3a71c6afe80d946d3ced52ecc57c53c9829791bfff1830ff5a1f0c"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, @@ -26,6 +30,7 @@ "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "owl": {:hex, :owl, "0.11.0", "2cd46185d330aa2400f1c8c3cddf8d2ff6320baeff23321d1810e58127082cae", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "73f5783f0e963cc04a061be717a0dbb3e49ae0c4bfd55fb4b78ece8d33a65efe"}, + "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{: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]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, "reactor": {:hex, :reactor, "0.9.1", "082f8e9b1fd7586c0a016c2fb533835fec7eaef5ffb0263abb4473106c20b1ca", [:mix], [{:igniter, "~> 0.2", [hex: :igniter, repo: "hexpm", optional: false]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7191ddf95fdd2b65770a57a2e38dd502a94909e51ac8daf497330e67fc032dc3"}, "rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"}, "simple_sat": {:hex, :simple_sat, "0.1.3", "f650fc3c184a5fe741868b5ac56dc77fdbb428468f6dbf1978e14d0334497578", [:mix], [], "hexpm", "a54305066a356b7194dc81db2a89232bacdc0b3edaef68ed9aba28dcbc34887b"}, diff --git a/priv/resource_snapshots/test_repo/extensions.json b/priv/resource_snapshots/test_repo/extensions.json new file mode 100644 index 0000000..c7b5254 --- /dev/null +++ b/priv/resource_snapshots/test_repo/extensions.json @@ -0,0 +1,6 @@ +{ + "ash_functions_version": 4, + "installed": [ + "ash-functions" + ] +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/posts/20240905173414.json b/priv/resource_snapshots/test_repo/posts/20240905173414.json new file mode 100644 index 0000000..41d359e --- /dev/null +++ b/priv/resource_snapshots/test_repo/posts/20240905173414.json @@ -0,0 +1,39 @@ +{ + "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": "archived_at", + "type": "utc_datetime_usec" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "89AA75CB853B0B701A5E6C2BC611C10DB1612064CE32E005DA70FBADC5B2983F", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshArchival.TestRepo", + "schema": null, + "table": "posts" +} \ No newline at end of file diff --git a/priv/test_repo/migrations/20240905173232_install_ash-functions_extension_4.exs b/priv/test_repo/migrations/20240905173232_install_ash-functions_extension_4.exs new file mode 100644 index 0000000..3c963fc --- /dev/null +++ b/priv/test_repo/migrations/20240905173232_install_ash-functions_extension_4.exs @@ -0,0 +1,139 @@ +defmodule AshArchival.TestRepo.Migrations.InstallAshFunctionsExtension420240905173230 do + @moduledoc """ + Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + execute(""" + CREATE OR REPLACE FUNCTION ash_elixir_or(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) + AS $$ SELECT COALESCE(NULLIF($1, FALSE), $2) $$ + LANGUAGE SQL + IMMUTABLE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_elixir_or(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) + AS $$ SELECT COALESCE($1, $2) $$ + LANGUAGE SQL + IMMUTABLE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_elixir_and(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$ + SELECT CASE + WHEN $1 IS TRUE THEN $2 + ELSE $1 + END $$ + LANGUAGE SQL + IMMUTABLE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_elixir_and(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$ + SELECT CASE + WHEN $1 IS NOT NULL THEN $2 + ELSE $1 + END $$ + LANGUAGE SQL + IMMUTABLE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_trim_whitespace(arr text[]) + RETURNS text[] AS $$ + DECLARE + start_index INT = 1; + end_index INT = array_length(arr, 1); + BEGIN + WHILE start_index <= end_index AND arr[start_index] = '' LOOP + start_index := start_index + 1; + END LOOP; + + WHILE end_index >= start_index AND arr[end_index] = '' LOOP + end_index := end_index - 1; + END LOOP; + + IF start_index > end_index THEN + RETURN ARRAY[]::text[]; + ELSE + RETURN arr[start_index : end_index]; + END IF; + END; $$ + LANGUAGE plpgsql + IMMUTABLE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb) + RETURNS BOOLEAN AS $$ + BEGIN + -- Raise an error with the provided JSON data. + -- The JSON object is converted to text for inclusion in the error message. + RAISE EXCEPTION 'ash_error: %', json_data::text; + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb, type_signal ANYCOMPATIBLE) + RETURNS ANYCOMPATIBLE AS $$ + BEGIN + -- Raise an error with the provided JSON data. + -- The JSON object is converted to text for inclusion in the error message. + RAISE EXCEPTION 'ash_error: %', json_data::text; + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + """) + + execute(""" + CREATE OR REPLACE FUNCTION uuid_generate_v7() + RETURNS UUID + AS $$ + DECLARE + timestamp TIMESTAMPTZ; + microseconds INT; + BEGIN + timestamp = clock_timestamp(); + microseconds = (cast(extract(microseconds FROM timestamp)::INT - (floor(extract(milliseconds FROM timestamp))::INT * 1000) AS DOUBLE PRECISION) * 4.096)::INT; + + RETURN encode( + set_byte( + set_byte( + overlay(uuid_send(gen_random_uuid()) placing substring(int8send(floor(extract(epoch FROM timestamp) * 1000)::BIGINT) FROM 3) FROM 1 FOR 6 + ), + 6, (b'0111' || (microseconds >> 8)::bit(4))::bit(8)::int + ), + 7, microseconds::bit(8)::int + ), + 'hex')::UUID; + END + $$ + LANGUAGE PLPGSQL + VOLATILE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION timestamp_from_uuid_v7(_uuid uuid) + RETURNS TIMESTAMP WITHOUT TIME ZONE + AS $$ + SELECT to_timestamp(('x0000' || substr(_uuid::TEXT, 1, 8) || substr(_uuid::TEXT, 10, 4))::BIT(64)::BIGINT::NUMERIC / 1000); + $$ + LANGUAGE SQL + IMMUTABLE PARALLEL SAFE STRICT LEAKPROOF; + """) + end + + def down do + # Uncomment this if you actually want to uninstall the extensions + # when this migration is rolled back: + execute( + "DROP FUNCTION IF EXISTS uuid_generate_v7(), timestamp_from_uuid_v7(uuid), ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE), ash_elixir_and(BOOLEAN, ANYCOMPATIBLE), ash_elixir_and(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(BOOLEAN, ANYCOMPATIBLE), ash_trim_whitespace(text[])" + ) + end +end diff --git a/priv/test_repo/migrations/20240905173414_add_posts.exs b/priv/test_repo/migrations/20240905173414_add_posts.exs new file mode 100644 index 0000000..855dcf3 --- /dev/null +++ b/priv/test_repo/migrations/20240905173414_add_posts.exs @@ -0,0 +1,20 @@ +defmodule AshArchival.TestRepo.Migrations.AddPosts 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 + create table(:posts, primary_key: false) do + add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true) + add(:archived_at, :utc_datetime_usec) + end + end + + def down do + drop(table(:posts)) + end +end diff --git a/test/archival_test.exs b/test/archival_test.exs index e178218..ce167de 100644 --- a/test/archival_test.exs +++ b/test/archival_test.exs @@ -75,6 +75,12 @@ defmodule ArchivalTest do create(:upsert) read(:all_posts) + + update :unarchive do + accept([]) + atomic_upgrade_with(:all_posts) + change(set_attribute(:archived_at, nil)) + end end attributes do @@ -100,6 +106,39 @@ defmodule ArchivalTest do end end + defmodule UnarchivablePost do + use Ash.Resource, + domain: ArchivalTest.Domain, + data_layer: Ash.DataLayer.Ets, + extensions: [AshArchival.Resource] + + ets do + table(:posts) + private?(true) + end + + archive do + exclude_read_actions :all_posts + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + + read(:all_posts) + + update :unarchive do + accept([]) + atomic_upgrade_with(:all_posts) + change(set_attribute(:archived_at, nil)) + end + end + + attributes do + uuid_primary_key(:id) + end + end + defmodule PostWithArchive do use Ash.Resource, domain: ArchivalTest.Domain, @@ -177,6 +216,7 @@ defmodule ArchivalTest do resource(Author) resource(AuthorWithArchive) resource(Post) + resource(UnarchivablePost) resource(PostWithArchive) resource(Comment) resource(CommentWithArchive) @@ -207,6 +247,15 @@ defmodule ArchivalTest do assert [] = Ash.read!(Post) end + test "archived records can be unarchived" do + assert %UnarchivablePost{} = + UnarchivablePost + |> Ash.Changeset.for_create(:create) + |> Ash.create!() + |> Ash.Changeset.for_update(:unarchive) + |> Ash.update!() + end + test "upserts don't consider archived records if included in the identity" do post = Post diff --git a/test/postgres_test.exs b/test/postgres_test.exs new file mode 100644 index 0000000..f3ff032 --- /dev/null +++ b/test/postgres_test.exs @@ -0,0 +1,35 @@ +defmodule AshArchival.PostgresTest do + use AshArchival.RepoCase + + alias AshArchival.Test.Post + require Ash.Query + + test "unarchival works" do + Logger.configure(level: :debug) + + assert %Post{} = + Post + |> Ash.Changeset.for_create(:create) + |> Ash.create!() + |> Ash.Changeset.for_destroy(:destroy) + |> Ash.destroy!(return_destroyed?: true) + |> Ash.Changeset.for_update(:unarchive) + |> Ash.update!() + end + + test "bulk unarchival works" do + Logger.configure(level: :debug) + + assert %Ash.BulkResult{records: [%Post{}]} = + Post + |> Ash.Changeset.for_create(:create) + |> Ash.create!() + |> Ash.Changeset.for_destroy(:destroy) + |> Ash.destroy!(return_destroyed?: true) + |> then(fn post -> + Post + |> Ash.Query.filter(id == ^post.id) + end) + |> Ash.bulk_update!(:unarchive, %{}, return_records?: true) + end +end diff --git a/test/support/domain.ex b/test/support/domain.ex new file mode 100644 index 0000000..ce091f6 --- /dev/null +++ b/test/support/domain.ex @@ -0,0 +1,8 @@ +defmodule AshArchival.Test.Domain do + @moduledoc false + use Ash.Domain + + resources do + resource(AshArchival.Test.Post) + end +end diff --git a/test/support/post.ex b/test/support/post.ex new file mode 100644 index 0000000..1d3adfd --- /dev/null +++ b/test/support/post.ex @@ -0,0 +1,33 @@ +defmodule AshArchival.Test.Post do + @moduledoc false + use Ash.Resource, + domain: AshArchival.Test.Domain, + data_layer: AshPostgres.DataLayer, + extensions: [AshArchival.Resource] + + archive do + exclude_read_actions :all_posts + end + + postgres do + table("posts") + repo(AshArchival.TestRepo) + end + + attributes do + uuid_primary_key(:id) + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + + read(:all_posts) + + update :unarchive do + accept([]) + atomic_upgrade_with(:all_posts) + change(set_attribute(:archived_at, nil)) + end + end +end diff --git a/test/support/repo.ex b/test/support/repo.ex new file mode 100644 index 0000000..be1b683 --- /dev/null +++ b/test/support/repo.ex @@ -0,0 +1,20 @@ +defmodule AshArchival.TestRepo do + @moduledoc false + use AshPostgres.Repo, + otp_app: :ash_archival + + def on_transaction_begin(data) do + send(self(), data) + end + + def installed_extensions do + ["ash-functions"] + end + + def min_pg_version do + case System.get_env("PG_VERSION") do + nil -> %Version{major: 16, minor: 0, patch: 0} + version -> Version.parse!(version) + end + end +end diff --git a/test/support/repo_case.ex b/test/support/repo_case.ex new file mode 100644 index 0000000..40fe78d --- /dev/null +++ b/test/support/repo_case.ex @@ -0,0 +1,28 @@ +defmodule AshArchival.RepoCase do + @moduledoc false + use ExUnit.CaseTemplate + + alias Ecto.Adapters.SQL.Sandbox + + using do + quote do + alias AshArchival.TestRepo + + import Ecto + import Ecto.Query + import AshArchival.RepoCase + + # and any other stuff + end + end + + setup tags do + :ok = Sandbox.checkout(AshArchival.TestRepo) + + unless tags[:async] do + Sandbox.mode(AshArchival.TestRepo, {:shared, self()}) + end + + :ok + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..e14d30b 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,3 @@ ExUnit.start() + +AshArchival.TestRepo.start_link()