From 129ecbf3529b1b98b27c9c0b625241fa76c6bb72 Mon Sep 17 00:00:00 2001 From: Joel Kociolek Date: Wed, 15 May 2024 16:36:43 +0300 Subject: [PATCH] chore: copy ash_sqlite, replace sqlite with mysql and make some tests pass --- .check.exs | 21 + .credo.exs | 184 ++ .formatter.exs | 45 + .git.orig/FETCH_HEAD | 2 + .git.orig/HEAD | 1 + .git.orig/ORIG_HEAD | 1 + .git.orig/config | 12 + .git.orig/description | 1 + .git.orig/hooks/applypatch-msg.sample | 15 + .git.orig/hooks/commit-msg.sample | 24 + .git.orig/hooks/fsmonitor-watchman.sample | 174 ++ .git.orig/hooks/post-update.sample | 8 + .git.orig/hooks/pre-applypatch.sample | 14 + .git.orig/hooks/pre-commit.sample | 49 + .git.orig/hooks/pre-merge-commit.sample | 13 + .git.orig/hooks/pre-push.sample | 53 + .git.orig/hooks/pre-rebase.sample | 169 ++ .git.orig/hooks/pre-receive.sample | 24 + .git.orig/hooks/prepare-commit-msg.sample | 42 + .git.orig/hooks/push-to-checkout.sample | 78 + .git.orig/hooks/sendemail-validate.sample | 77 + .git.orig/hooks/update.sample | 128 + .git.orig/index | Bin 0 -> 12709 bytes .git.orig/info/exclude | 6 + .git.orig/logs/HEAD | 2 + .git.orig/logs/refs/heads/main | 2 + .git.orig/logs/refs/remotes/origin/HEAD | 1 + .git.orig/logs/refs/remotes/origin/main | 1 + .../04/67fd09bbd2872c1e661f3cf0c273ef9aee72ae | Bin 0 -> 1826 bytes .../0f/4042338f7a55716550278872a2ce096b089162 | Bin 0 -> 93 bytes .../20/a3c392517ad8f3e00a75d350bc8e9f55afd23c | 4 + .../23/b17f5fb882b1845b1dd3bd276509d086f658e9 | Bin 0 -> 585 bytes .../2b/ed9af095c876ef9131468f6305f761e51c7869 | Bin 0 -> 1471 bytes .../3c/75dddf64c953dead4c6e27a9b8d299b013c471 | Bin 0 -> 1538 bytes .../40/8676a416aa889991156047b423a4def5f4625c | Bin 0 -> 5012 bytes .../4c/a02b3951a7edd86698d6becdbaf38a2e0f2e30 | Bin 0 -> 684 bytes .../56/09cc7941235f99ae9a3ab723fa1c0855c62351 | 2 + .../56/1018539ac44bb4e74ab4ab3fae65d93bfde262 | Bin 0 -> 5172 bytes .../58/b07aeebdc1b4a0da7eb94a92e1bc655ec19b50 | Bin 0 -> 873 bytes .../58/e3843da16dab7e0167bae41fb18d787fd78d1c | Bin 0 -> 1829 bytes .../5c/f1b8583f408862afb161ae09fc7634a46f34a7 | Bin 0 -> 154 bytes .../5f/49882907b36d837a577593928eca824b5547d3 | Bin 0 -> 320 bytes .../6c/9f5050a116ece485f0eb8914d98425e049bd92 | Bin 0 -> 585 bytes .../7a/06bde184c49014feb523e9a54b726df8680fc2 | 2 + .../7a/dbb168debec28b54ccdaac3a082e52eb78bae7 | Bin 0 -> 1847 bytes .../85/0db890df9cb03de53e701189b775866254437e | Bin 0 -> 550 bytes .../8f/d882434ce2289e9789684588c0ce48cb1c4657 | Bin 0 -> 5019 bytes .../a9/3196823aa13eb6bcf524b9fed8bffc97f0a4e9 | Bin 0 -> 537 bytes .../b3/2de3bf69b67f9442f32a615efa5ae8a470ed10 | Bin 0 -> 585 bytes .../b8/92bd45591435a61d4fc57b954841a8bfdd5a81 | 1 + .../b8/c859533235b2d9acf2541de0619342983e2192 | Bin 0 -> 585 bytes .../bb/f1d65aa293a49f5da080c0a91594a4c2cfdebf | Bin 0 -> 82 bytes .../be/e6119cdeea9728ec3144ccbb18bc1d8a239deb | Bin 0 -> 79 bytes .../c4/6d55c9446904e699f35a48892de809f4061166 | 2 + .../c4/cb735515e9ed67db62710b6998d8eb9af776db | Bin 0 -> 55 bytes .../c6/78d7bbcbfe040f850a951876de535b837b13f9 | Bin 0 -> 5183 bytes .../d9/517e6ccbacc258955b752d68a52014a2d8f223 | Bin 0 -> 586 bytes .../df/27b7e0e94f44b672c0c077dcfd666b38300bcb | 2 + .../f6/4965c0271738423a6d10c10be61b2245b628d7 | Bin 0 -> 53 bytes .../f6/69686b38ada693234a078c22481a3774e36cf2 | Bin 0 -> 12812 bytes .../fc/428bc75ab0198ea2b3ca70cc0311b95381779b | Bin 0 -> 3646 bytes .../fc/ad7b900f8a691fc0730c61b5befd38aee71e36 | Bin 0 -> 1828 bytes .../fe/94b91c7bccecf0ce98fc393b914f1de4f05a41 | Bin 0 -> 109 bytes ...1e412ea3833a4bbedf2c966c4755fc019c8240.idx | Bin 0 -> 22380 bytes ...e412ea3833a4bbedf2c966c4755fc019c8240.pack | Bin 0 -> 274001 bytes ...1e412ea3833a4bbedf2c966c4755fc019c8240.rev | Bin 0 -> 3096 bytes .git.orig/packed-refs | 11 + .git.orig/refs/heads/main | 1 + .git.orig/refs/remotes/origin/HEAD | 1 + .git.orig/refs/remotes/origin/main | 1 + .git.orig/refs/tags/v0.1.2 | 1 + .github/CODE_OF_CONDUCT.md | 76 + .github/CONTRIBUTING.md | 2 + .github/ISSUE_TEMPLATE/bug_report.md | 27 + .github/ISSUE_TEMPLATE/feature_request.md | 35 + .github/PULL_REQUEST_TEMPLATE.md | 4 + .github/dependabot.yml | 6 + .github/workflows/elixir.yml | 15 + .gitignore | 30 + .tool-versions | 2 + .vscode/settings.json | 6 + CHANGELOG.md | 61 + LICENSE | 21 + README.md | 38 + config/config.exs | 54 + .../dsls/DSL:-AshSqlite.DataLayer.md | 280 ++ .../about-ash-sqlite/what-is-ash-sqlite.md | 34 + documentation/topics/advanced/expressions.md | 61 + .../topics/advanced/manual-relationships.md | 87 + .../development/migrations-and-tasks.md | 98 + documentation/topics/development/testing.md | 11 + .../topics/resources/polymorphic-resources.md | 85 + documentation/topics/resources/references.md | 23 + .../getting-started-with-ash-sqlite.md | 277 ++ lib/ash_sqlite.ex | 7 + lib/custom_extension.ex | 20 + lib/custom_index.ex | 109 + lib/data_layer.ex | 1589 +++++++++++ lib/data_layer/info.ex | 117 + lib/functions/ilike.ex | 9 + lib/functions/like.ex | 9 + lib/manual_relationship.ex | 25 + .../migration_generator.ex | 2542 +++++++++++++++++ lib/migration_generator/operation.ex | 784 +++++ lib/migration_generator/phase.ex | 66 + lib/mix/helpers.ex | 132 + lib/mix/tasks/ash_sqlite.create.ex | 50 + lib/mix/tasks/ash_sqlite.drop.ex | 58 + .../tasks/ash_sqlite.generate_migrations.ex | 95 + lib/mix/tasks/ash_sqlite.migrate.ex | 116 + lib/mix/tasks/ash_sqlite.rollback.ex | 81 + lib/reference.ex | 43 + lib/repo.ex | 155 + lib/sql_implementation.ex | 449 +++ lib/statement.ex | 45 + .../ensure_table_or_polymorphic.ex | 30 + lib/transformers/validate_references.ex | 23 + lib/transformers/verify_repo.ex | 22 + lib/type.ex | 19 + lib/types/types.ex | 182 ++ logos/small-logo.png | Bin 0 -> 3088 bytes mix.exs | 203 ++ mix.lock | 42 + .../test_repo/accounts/20240405234211.json | 62 + .../test_repo/authors/20240405234211.json | 70 + .../comment_ratings/20240405234211.json | 62 + .../test_repo/comments/20240405234211.json | 117 + .../integer_posts/20240405234211.json | 37 + .../test_repo/managers/20240405234211.json | 101 + .../test_repo/orgs/20240405234211.json | 37 + .../test_repo/post_links/20240405234211.json | 87 + .../post_ratings/20240405234211.json | 62 + .../test_repo/post_views/20240405234211.json | 47 + .../test_repo/posts/20240405234211.json | 261 ++ .../test_repo/profile/20240405234211.json | 62 + .../test_repo/users/20240405234211.json | 62 + .../20240405234211_migrate_resources1.exs | 231 ++ test/atomics_test.exs | 75 + test/bulk_create_test.exs | 143 + test/calculation_test.exs | 274 ++ test/custom_index_test.exs | 28 + test/ecto_compatibility_test.exs | 15 + test/embeddable_resource_test.exs | 34 + test/enum_test.exs | 13 + test/filter_test.exs | 655 +++++ test/load_test.exs | 247 ++ test/manual_relationships_test.exs | 116 + test/migration_generator_test.exs | 816 ++++++ test/polymorphism_test.exs | 29 + test/primary_key_test.exs | 52 + test/select_test.exs | 15 + test/sort_test.exs | 175 ++ test/support/concat.ex | 35 + test/support/domain.ex | 23 + .../comments_containing_title.ex | 48 + test/support/repo_case.ex | 28 + test/support/resources/account.ex | 32 + test/support/resources/author.ex | 74 + test/support/resources/bio.ex | 21 + test/support/resources/comment.ex | 63 + test/support/resources/integer_post.ex | 21 + test/support/resources/manager.ex | 42 + test/support/resources/organization.ex | 21 + test/support/resources/post.ex | 235 ++ test/support/resources/post_link.ex | 42 + test/support/resources/post_views.ex | 35 + test/support/resources/profile.ex | 25 + test/support/resources/rating.ex | 22 + test/support/resources/user.ex | 24 + test/support/test_app.ex | 13 + test/support/test_custom_extension.ex | 38 + test/support/test_repo.ex | 5 + test/support/types/email.ex | 8 + test/support/types/money.ex | 18 + test/support/types/status.ex | 6 + test/support/types/status_enum.ex | 6 + test/support/types/status_enum_no_cast.ex | 8 + test/test_helper.exs | 6 + test/type_test.exs | 14 + test/unique_identity_test.exs | 45 + test/update_test.exs | 46 + test/upsert_test.exs | 60 + 182 files changed, 14579 insertions(+) create mode 100644 .check.exs create mode 100644 .credo.exs create mode 100644 .formatter.exs create mode 100644 .git.orig/FETCH_HEAD create mode 100644 .git.orig/HEAD create mode 100644 .git.orig/ORIG_HEAD create mode 100644 .git.orig/config create mode 100644 .git.orig/description create mode 100755 .git.orig/hooks/applypatch-msg.sample create mode 100755 .git.orig/hooks/commit-msg.sample create mode 100755 .git.orig/hooks/fsmonitor-watchman.sample create mode 100755 .git.orig/hooks/post-update.sample create mode 100755 .git.orig/hooks/pre-applypatch.sample create mode 100755 .git.orig/hooks/pre-commit.sample create mode 100755 .git.orig/hooks/pre-merge-commit.sample create mode 100755 .git.orig/hooks/pre-push.sample create mode 100755 .git.orig/hooks/pre-rebase.sample create mode 100755 .git.orig/hooks/pre-receive.sample create mode 100755 .git.orig/hooks/prepare-commit-msg.sample create mode 100755 .git.orig/hooks/push-to-checkout.sample create mode 100755 .git.orig/hooks/sendemail-validate.sample create mode 100755 .git.orig/hooks/update.sample create mode 100644 .git.orig/index create mode 100644 .git.orig/info/exclude create mode 100644 .git.orig/logs/HEAD create mode 100644 .git.orig/logs/refs/heads/main create mode 100644 .git.orig/logs/refs/remotes/origin/HEAD create mode 100644 .git.orig/logs/refs/remotes/origin/main create mode 100644 .git.orig/objects/04/67fd09bbd2872c1e661f3cf0c273ef9aee72ae create mode 100644 .git.orig/objects/0f/4042338f7a55716550278872a2ce096b089162 create mode 100644 .git.orig/objects/20/a3c392517ad8f3e00a75d350bc8e9f55afd23c create mode 100644 .git.orig/objects/23/b17f5fb882b1845b1dd3bd276509d086f658e9 create mode 100644 .git.orig/objects/2b/ed9af095c876ef9131468f6305f761e51c7869 create mode 100644 .git.orig/objects/3c/75dddf64c953dead4c6e27a9b8d299b013c471 create mode 100644 .git.orig/objects/40/8676a416aa889991156047b423a4def5f4625c create mode 100644 .git.orig/objects/4c/a02b3951a7edd86698d6becdbaf38a2e0f2e30 create mode 100644 .git.orig/objects/56/09cc7941235f99ae9a3ab723fa1c0855c62351 create mode 100644 .git.orig/objects/56/1018539ac44bb4e74ab4ab3fae65d93bfde262 create mode 100644 .git.orig/objects/58/b07aeebdc1b4a0da7eb94a92e1bc655ec19b50 create mode 100644 .git.orig/objects/58/e3843da16dab7e0167bae41fb18d787fd78d1c create mode 100644 .git.orig/objects/5c/f1b8583f408862afb161ae09fc7634a46f34a7 create mode 100644 .git.orig/objects/5f/49882907b36d837a577593928eca824b5547d3 create mode 100644 .git.orig/objects/6c/9f5050a116ece485f0eb8914d98425e049bd92 create mode 100644 .git.orig/objects/7a/06bde184c49014feb523e9a54b726df8680fc2 create mode 100644 .git.orig/objects/7a/dbb168debec28b54ccdaac3a082e52eb78bae7 create mode 100644 .git.orig/objects/85/0db890df9cb03de53e701189b775866254437e create mode 100644 .git.orig/objects/8f/d882434ce2289e9789684588c0ce48cb1c4657 create mode 100644 .git.orig/objects/a9/3196823aa13eb6bcf524b9fed8bffc97f0a4e9 create mode 100644 .git.orig/objects/b3/2de3bf69b67f9442f32a615efa5ae8a470ed10 create mode 100644 .git.orig/objects/b8/92bd45591435a61d4fc57b954841a8bfdd5a81 create mode 100644 .git.orig/objects/b8/c859533235b2d9acf2541de0619342983e2192 create mode 100644 .git.orig/objects/bb/f1d65aa293a49f5da080c0a91594a4c2cfdebf create mode 100644 .git.orig/objects/be/e6119cdeea9728ec3144ccbb18bc1d8a239deb create mode 100644 .git.orig/objects/c4/6d55c9446904e699f35a48892de809f4061166 create mode 100644 .git.orig/objects/c4/cb735515e9ed67db62710b6998d8eb9af776db create mode 100644 .git.orig/objects/c6/78d7bbcbfe040f850a951876de535b837b13f9 create mode 100644 .git.orig/objects/d9/517e6ccbacc258955b752d68a52014a2d8f223 create mode 100644 .git.orig/objects/df/27b7e0e94f44b672c0c077dcfd666b38300bcb create mode 100644 .git.orig/objects/f6/4965c0271738423a6d10c10be61b2245b628d7 create mode 100644 .git.orig/objects/f6/69686b38ada693234a078c22481a3774e36cf2 create mode 100644 .git.orig/objects/fc/428bc75ab0198ea2b3ca70cc0311b95381779b create mode 100644 .git.orig/objects/fc/ad7b900f8a691fc0730c61b5befd38aee71e36 create mode 100644 .git.orig/objects/fe/94b91c7bccecf0ce98fc393b914f1de4f05a41 create mode 100644 .git.orig/objects/pack/pack-011e412ea3833a4bbedf2c966c4755fc019c8240.idx create mode 100644 .git.orig/objects/pack/pack-011e412ea3833a4bbedf2c966c4755fc019c8240.pack create mode 100644 .git.orig/objects/pack/pack-011e412ea3833a4bbedf2c966c4755fc019c8240.rev create mode 100644 .git.orig/packed-refs create mode 100644 .git.orig/refs/heads/main create mode 100644 .git.orig/refs/remotes/origin/HEAD create mode 100644 .git.orig/refs/remotes/origin/main create mode 100644 .git.orig/refs/tags/v0.1.2 create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/elixir.yml create mode 100644 .gitignore create mode 100644 .tool-versions create mode 100644 .vscode/settings.json create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config/config.exs create mode 100644 documentation/dsls/DSL:-AshSqlite.DataLayer.md create mode 100644 documentation/topics/about-ash-sqlite/what-is-ash-sqlite.md create mode 100644 documentation/topics/advanced/expressions.md create mode 100644 documentation/topics/advanced/manual-relationships.md create mode 100644 documentation/topics/development/migrations-and-tasks.md create mode 100644 documentation/topics/development/testing.md create mode 100644 documentation/topics/resources/polymorphic-resources.md create mode 100644 documentation/topics/resources/references.md create mode 100644 documentation/tutorials/getting-started-with-ash-sqlite.md create mode 100644 lib/ash_sqlite.ex create mode 100644 lib/custom_extension.ex create mode 100644 lib/custom_index.ex create mode 100644 lib/data_layer.ex create mode 100644 lib/data_layer/info.ex create mode 100644 lib/functions/ilike.ex create mode 100644 lib/functions/like.ex create mode 100644 lib/manual_relationship.ex create mode 100644 lib/migration_generator/migration_generator.ex create mode 100644 lib/migration_generator/operation.ex create mode 100644 lib/migration_generator/phase.ex create mode 100644 lib/mix/helpers.ex create mode 100644 lib/mix/tasks/ash_sqlite.create.ex create mode 100644 lib/mix/tasks/ash_sqlite.drop.ex create mode 100644 lib/mix/tasks/ash_sqlite.generate_migrations.ex create mode 100644 lib/mix/tasks/ash_sqlite.migrate.ex create mode 100644 lib/mix/tasks/ash_sqlite.rollback.ex create mode 100644 lib/reference.ex create mode 100644 lib/repo.ex create mode 100644 lib/sql_implementation.ex create mode 100644 lib/statement.ex create mode 100644 lib/transformers/ensure_table_or_polymorphic.ex create mode 100644 lib/transformers/validate_references.ex create mode 100644 lib/transformers/verify_repo.ex create mode 100644 lib/type.ex create mode 100644 lib/types/types.ex create mode 100644 logos/small-logo.png create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 priv/resource_snapshots/test_repo/accounts/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/authors/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/comment_ratings/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/comments/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/integer_posts/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/managers/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/orgs/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/post_links/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/post_ratings/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/post_views/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/posts/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/profile/20240405234211.json create mode 100644 priv/resource_snapshots/test_repo/users/20240405234211.json create mode 100644 priv/test_repo/migrations/20240405234211_migrate_resources1.exs create mode 100644 test/atomics_test.exs create mode 100644 test/bulk_create_test.exs create mode 100644 test/calculation_test.exs create mode 100644 test/custom_index_test.exs create mode 100644 test/ecto_compatibility_test.exs create mode 100644 test/embeddable_resource_test.exs create mode 100644 test/enum_test.exs create mode 100644 test/filter_test.exs create mode 100644 test/load_test.exs create mode 100644 test/manual_relationships_test.exs create mode 100644 test/migration_generator_test.exs create mode 100644 test/polymorphism_test.exs create mode 100644 test/primary_key_test.exs create mode 100644 test/select_test.exs create mode 100644 test/sort_test.exs create mode 100644 test/support/concat.ex create mode 100644 test/support/domain.ex create mode 100644 test/support/relationships/comments_containing_title.ex create mode 100644 test/support/repo_case.ex create mode 100644 test/support/resources/account.ex create mode 100644 test/support/resources/author.ex create mode 100644 test/support/resources/bio.ex create mode 100644 test/support/resources/comment.ex create mode 100644 test/support/resources/integer_post.ex create mode 100644 test/support/resources/manager.ex create mode 100644 test/support/resources/organization.ex create mode 100644 test/support/resources/post.ex create mode 100644 test/support/resources/post_link.ex create mode 100644 test/support/resources/post_views.ex create mode 100644 test/support/resources/profile.ex create mode 100644 test/support/resources/rating.ex create mode 100644 test/support/resources/user.ex create mode 100644 test/support/test_app.ex create mode 100644 test/support/test_custom_extension.ex create mode 100644 test/support/test_repo.ex create mode 100644 test/support/types/email.ex create mode 100644 test/support/types/money.ex create mode 100644 test/support/types/status.ex create mode 100644 test/support/types/status_enum.ex create mode 100644 test/support/types/status_enum_no_cast.ex create mode 100644 test/test_helper.exs create mode 100644 test/type_test.exs create mode 100644 test/unique_identity_test.exs create mode 100644 test/update_test.exs create mode 100644 test/upsert_test.exs diff --git a/.check.exs b/.check.exs new file mode 100644 index 0000000..91e14cb --- /dev/null +++ b/.check.exs @@ -0,0 +1,21 @@ +[ + ## all available options with default values (see `mix check` docs for description) + # parallel: true, + # skipped: true, + retry: false, + ## list of tools (see `mix check` docs for defaults) + tools: [ + ## curated tools may be disabled (e.g. the check for compilation warnings) + # {:compiler, false}, + + ## ...or adjusted (e.g. use one-line formatter for more compact credo output) + # {:credo, "mix credo --format oneline"}, + + {:check_formatter, command: "mix spark.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"}, + # {:my_arbitrary_script, command: ["my_script", "argument with spaces"], cd: "scripts"} + ] +] diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..a986fd0 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,184 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, false}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, false}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, false}, + {Credo.Check.Design.TagFIXME, []}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, false}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, false}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapInto, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, [max_nesting: 5]}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.MixEnv, false}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.UnsafeExec, []}, + + # + # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + + # + # Controversial and experimental checks (opt-in, just replace `false` with `[]`) + # + {Credo.Check.Readability.StrictModuleLayout, false}, + {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, + {Credo.Check.Consistency.UnusedVariableNames, false}, + {Credo.Check.Design.DuplicatedCode, false}, + {Credo.Check.Readability.AliasAs, false}, + {Credo.Check.Readability.MultiAlias, false}, + {Credo.Check.Readability.Specs, false}, + {Credo.Check.Readability.SinglePipe, false}, + {Credo.Check.Readability.WithCustomTaggedTuple, false}, + {Credo.Check.Refactor.ABCSize, false}, + {Credo.Check.Refactor.AppendSingleItem, false}, + {Credo.Check.Refactor.DoubleBooleanNegation, false}, + {Credo.Check.Refactor.ModuleDependencies, false}, + {Credo.Check.Refactor.NegatedIsNil, false}, + {Credo.Check.Refactor.PipeChainStart, false}, + {Credo.Check.Refactor.VariableRebinding, false}, + {Credo.Check.Warning.LeakyEnvironment, false}, + {Credo.Check.Warning.MapGetUnsafePass, false}, + {Credo.Check.Warning.UnsafeToAtom, false} + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + ] +} diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..18c9078 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,45 @@ +spark_locals_without_parens = [ + base_filter_sql: 1, + code?: 1, + deferrable: 1, + down: 1, + exclusion_constraint_names: 1, + foreign_key_names: 1, + identity_index_names: 1, + ignore?: 1, + include: 1, + index: 1, + index: 2, + message: 1, + migrate?: 1, + migration_defaults: 1, + migration_ignore_attributes: 1, + migration_types: 1, + name: 1, + on_delete: 1, + on_update: 1, + polymorphic?: 1, + polymorphic_name: 1, + polymorphic_on_delete: 1, + polymorphic_on_update: 1, + reference: 1, + reference: 2, + repo: 1, + skip_unique_indexes: 1, + statement: 1, + statement: 2, + table: 1, + unique: 1, + unique_index_names: 1, + up: 1, + using: 1, + where: 1 +] + +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + locals_without_parens: spark_locals_without_parens, + export: [ + locals_without_parens: spark_locals_without_parens + ] +] diff --git a/.git.orig/FETCH_HEAD b/.git.orig/FETCH_HEAD new file mode 100644 index 0000000..9ed37e0 --- /dev/null +++ b/.git.orig/FETCH_HEAD @@ -0,0 +1,2 @@ +58b07aeebdc1b4a0da7eb94a92e1bc655ec19b50 branch 'main' of github.com:ash-project/ash_sqlite +5609cc7941235f99ae9a3ab723fa1c0855c62351 not-for-merge tag 'v0.1.2' of github.com:ash-project/ash_sqlite diff --git a/.git.orig/HEAD b/.git.orig/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/.git.orig/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/.git.orig/ORIG_HEAD b/.git.orig/ORIG_HEAD new file mode 100644 index 0000000..eac8339 --- /dev/null +++ b/.git.orig/ORIG_HEAD @@ -0,0 +1 @@ +4e9cff586684056eb71ae7acad0fdbd7b5db541c diff --git a/.git.orig/config b/.git.orig/config new file mode 100644 index 0000000..9d03d1a --- /dev/null +++ b/.git.orig/config @@ -0,0 +1,12 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true +[remote "origin"] + url = git@github.com:ash-project/ash_sqlite.git + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "main"] + remote = origin + merge = refs/heads/main diff --git a/.git.orig/description b/.git.orig/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/.git.orig/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/.git.orig/hooks/applypatch-msg.sample b/.git.orig/hooks/applypatch-msg.sample new file mode 100755 index 0000000..a5d7b84 --- /dev/null +++ b/.git.orig/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/.git.orig/hooks/commit-msg.sample b/.git.orig/hooks/commit-msg.sample new file mode 100755 index 0000000..b58d118 --- /dev/null +++ b/.git.orig/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/.git.orig/hooks/fsmonitor-watchman.sample b/.git.orig/hooks/fsmonitor-watchman.sample new file mode 100755 index 0000000..23e856f --- /dev/null +++ b/.git.orig/hooks/fsmonitor-watchman.sample @@ -0,0 +1,174 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + my $last_update_line = ""; + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + $last_update_line = qq[\n"since": $last_update_token,]; + } + my $query = <<" END"; + ["query", "$git_work_tree", {$last_update_line + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/.git.orig/hooks/post-update.sample b/.git.orig/hooks/post-update.sample new file mode 100755 index 0000000..ec17ec1 --- /dev/null +++ b/.git.orig/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/.git.orig/hooks/pre-applypatch.sample b/.git.orig/hooks/pre-applypatch.sample new file mode 100755 index 0000000..4142082 --- /dev/null +++ b/.git.orig/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/.git.orig/hooks/pre-commit.sample b/.git.orig/hooks/pre-commit.sample new file mode 100755 index 0000000..29ed5ee --- /dev/null +++ b/.git.orig/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff-index --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/.git.orig/hooks/pre-merge-commit.sample b/.git.orig/hooks/pre-merge-commit.sample new file mode 100755 index 0000000..399eab1 --- /dev/null +++ b/.git.orig/hooks/pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/.git.orig/hooks/pre-push.sample b/.git.orig/hooks/pre-push.sample new file mode 100755 index 0000000..4ce688d --- /dev/null +++ b/.git.orig/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/.git.orig/hooks/pre-rebase.sample b/.git.orig/hooks/pre-rebase.sample new file mode 100755 index 0000000..6cbef5c --- /dev/null +++ b/.git.orig/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/.git.orig/hooks/pre-receive.sample b/.git.orig/hooks/pre-receive.sample new file mode 100755 index 0000000..a1fd29e --- /dev/null +++ b/.git.orig/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/.git.orig/hooks/prepare-commit-msg.sample b/.git.orig/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000..10fa14c --- /dev/null +++ b/.git.orig/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/.git.orig/hooks/push-to-checkout.sample b/.git.orig/hooks/push-to-checkout.sample new file mode 100755 index 0000000..af5a0c0 --- /dev/null +++ b/.git.orig/hooks/push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + exit 1 +} + +unset GIT_DIR GIT_WORK_TREE +cd "$worktree" && + +if grep -q "^diff --git " "$1" +then + validate_patch "$1" +else + validate_cover_letter "$1" +fi && + +if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL" +then + git config --unset-all sendemail.validateWorktree && + trap 'git worktree remove -ff "$worktree"' EXIT && + validate_series +fi diff --git a/.git.orig/hooks/update.sample b/.git.orig/hooks/update.sample new file mode 100755 index 0000000..c4d426b --- /dev/null +++ b/.git.orig/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to block unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/.git.orig/index b/.git.orig/index new file mode 100644 index 0000000000000000000000000000000000000000..2e8cd4dd4165e6e99a2fb81d0e1de50b40b9d792 GIT binary patch literal 12709 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 literal 0 HcmV?d00001 diff --git a/.git.orig/info/exclude b/.git.orig/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/.git.orig/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/.git.orig/logs/HEAD b/.git.orig/logs/HEAD new file mode 100644 index 0000000..8d141e4 --- /dev/null +++ b/.git.orig/logs/HEAD @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 4e9cff586684056eb71ae7acad0fdbd7b5db541c Joel Kociolek 1715158016 +0300 clone: from github.com:ash-project/ash_sqlite.git +4e9cff586684056eb71ae7acad0fdbd7b5db541c 58b07aeebdc1b4a0da7eb94a92e1bc655ec19b50 Joel Kociolek 1715508288 +0300 pull --autostash: Fast-forward diff --git a/.git.orig/logs/refs/heads/main b/.git.orig/logs/refs/heads/main new file mode 100644 index 0000000..8d141e4 --- /dev/null +++ b/.git.orig/logs/refs/heads/main @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 4e9cff586684056eb71ae7acad0fdbd7b5db541c Joel Kociolek 1715158016 +0300 clone: from github.com:ash-project/ash_sqlite.git +4e9cff586684056eb71ae7acad0fdbd7b5db541c 58b07aeebdc1b4a0da7eb94a92e1bc655ec19b50 Joel Kociolek 1715508288 +0300 pull --autostash: Fast-forward diff --git a/.git.orig/logs/refs/remotes/origin/HEAD b/.git.orig/logs/refs/remotes/origin/HEAD new file mode 100644 index 0000000..467dda9 --- /dev/null +++ b/.git.orig/logs/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 4e9cff586684056eb71ae7acad0fdbd7b5db541c Joel Kociolek 1715158016 +0300 clone: from github.com:ash-project/ash_sqlite.git diff --git a/.git.orig/logs/refs/remotes/origin/main b/.git.orig/logs/refs/remotes/origin/main new file mode 100644 index 0000000..6baeafe --- /dev/null +++ b/.git.orig/logs/refs/remotes/origin/main @@ -0,0 +1 @@ +4e9cff586684056eb71ae7acad0fdbd7b5db541c 58b07aeebdc1b4a0da7eb94a92e1bc655ec19b50 Joel Kociolek 1715508288 +0300 pull --autostash: fast-forward diff --git a/.git.orig/objects/04/67fd09bbd2872c1e661f3cf0c273ef9aee72ae b/.git.orig/objects/04/67fd09bbd2872c1e661f3cf0c273ef9aee72ae new file mode 100644 index 0000000000000000000000000000000000000000..f856ed250da2a0449e71716a148eb80f06641ed4 GIT binary patch literal 1826 zcmV+-2i^F10mWEtZ`(Ey-sk-aPQ}o)h~*^h2FyT<1#Q{{NZT#W_9X}eTB0p3vZ#?% z;w)%?`<|mDigx5KT`?>HVu!qUK6kw1tWY!d!;jCOeUpj#Qe|}^*rCa%e-_e;_?=w+ zqSYTFwJcNLGFBVGuokzA-(unqx4k_Qeq$>*Fqa7U@zW}58N8;VO6M{u?vvW53i~Tn{bmS#@V^suD4PqUS78$ z3b~Sc!lF+v*?zpgA9WkXa&41J=p`>jX(!G*QC{ynVJ}{=3G`g4yu3<_xzJindHUoUjj$oVTQ4)unEty1n5BsFD5{>+I29(-=$n#-V>ryI&u8% zPn;K$8-eBaOTSYgG<+c@to_pO7**?3B(*Mpz}#A>ACH@Zic_^51IVKm>GZt?wZ~|# zmZE~>e=Q)zVW>hP7exX+R;7wSn$Q7sh$u4x@=%PZ- zqFok|Ib%~-mC`mMy9XGPHKt2)%Tcwb)EV8GT%tG)))x3FN_X-X(YurL$U|)|3&O*v zn;lriVwAZhw^ry_tPBIJ+~?WR;p=xtPuc0otE2bRqo>Yi#$FvCzJGgk`r++U*pb#t zlpq&OUl0meUP1WxcP#^BZ>2r1XI})QmbVFbaJ()K1nh1s?G-8(D6y z9%M!+gE9+2eTZtFU2}Mvjl~Mrm!WI?LFzmouHKp#@V9W-H@U1nFCa5G_Se(X$*3B{2>yKbvxL@wwW4$HunW)t8jz7quX#Qp;`cg^tHuS_Ri{Cjcodr-D5_3{otxSam z0ASrF${P}&l=CgdzN(E?%cGUW)c{e$9yLTKWhPd`1ZULrQMDM}p5jgrl-WbBoZnX4 zqd}WPOhh z-^7setRcYUaj;1-$odC2IO=wm*b7;>$#)t4B<&!w(-e^sQ8=F9Ydfm7qLz9vvJEGwd>5msV1%Gsnq@bpfuC z??miB9~Kd?V5-Q_Nk=bVu)P?+!wjF%>AH0cXkR14s2Og+o39Z8blJSAhLEZFF!Pv{Mi z)Y;Mx+OL!sQAl|~<0pEmIHvi6jt6FFnc?`Yu7zug9V<0{>zn`}82@=_p}`X^cxyxJ zCA-N*$)rhUvgG=9$2%W*K`HX+uHE_QCVw)elMqn0OHRf8ae;yzX46J=d>FR|94{9C zVbIC8|0M7v`2P=nGZk;v;5H{G9+%S z!${Tlvq}3pp!Coh<}5#CxQYbax=&JOwM?zwrkN+a_##tryhow#ahsT2zz|Q{Odf0h zH`H2G1;ApZ6-DxO>LoY#5o96U5IYiMlDeh^24s`QH>K_EaO2^cP(;3sh zjD`H&>blb4nJgss%j?uYX!GP2(zc81l-+yRMdO=$MZ-nq?CTiXe>=l2*Cn(MZ=oi) QH&3e3n!~wfrH%7q?KH^X

YjjABKpg%7C literal 0 HcmV?d00001 diff --git a/.git.orig/objects/20/a3c392517ad8f3e00a75d350bc8e9f55afd23c b/.git.orig/objects/20/a3c392517ad8f3e00a75d350bc8e9f55afd23c new file mode 100644 index 0000000..cb44b54 --- /dev/null +++ b/.git.orig/objects/20/a3c392517ad8f3e00a75d350bc8e9f55afd23c @@ -0,0 +1,4 @@ +x}SIÏ£FÌ™_ÑÒ\f„æs·ÙL4‰†Åf1`³›(Šhh 6˜Ý6þõq)RåIïðê©UªJ›º.G€³üaì ‚®ÄŒÅ„Xij9æ¸$g¹ŒcÄ\@L’QÌ‘¡Ú¤'·°DLóœ[ñüŠ…O°€"$i’Á<Ù€¹ s,J©d‹¦iÉ-Kp3þòÞ_Á7VäE‘aú_ŸïÓ@úáãÖô¤­æs9þH›úg€Ä!å8|…,„Ô}«I´rÔ' ¾ýMûþ¿´s{Ê3øúÇÈkÍpÀ^ÛßÐ)½õŸ8(ð6©,I²"I®ìšõŽ™ÅSPâ +ÓÄ…E%IRå6®¤báD<1:pãhk[3¢À‘$Ã:f¯*b&1nͧyvà)ÈlF2ú—i§Ohí3UJ0ON(d·k¢z?a#ØPƘׯ…&É~`tgáu}\שt;™ÛÎEäØ7S)®Jg97-Tiü–çÍú¶ë8x¹¼}X#]­×”™Î;C¯±ê½ª†Ëx©.*wë K×ÂÖZÞùe ²¬¥yÒ6·ñ¾›‹ü­¯ç&k–ï5õ•²ß;½åîâé4%Ä’ƒçu'wË;ô —wï N\ÂF´õ«¤í8 +W¯çµ«Úba:5 ¶rúòŒ×‘÷”ã*ì«C¿8¬wfò<`z~`<ÌÀRnÙYª“å ·j ‘‘Øô%´£ßýx*»!óX  8s8]Ôe+¸ÐïôÒÞîW“7ؼDO.ʺ ¾¸*1\$œ~/´r—[0ìÜkT²Ò9;+g#J»˜÷(0ÓÎKÛl“ã.ìGZ·n,wÚVÌìGîcµ¨äD-¼XåV‚á– ^xú.VFÿBa¢€91••.öÔ+#ZŸÙ QüÒ˜öçQ AkvL˜Î²ß»Æž=ö®Tè6pòwÚ ÕVÚÛÛNS¦ŽUú¸§ÀOì~䨿:±vÔÿn•¾«L>¿;|ùà©nA2¿ ]ò¾©ü@èkŸ~ ŒÍ?ç‚ÏŸô…¢~Ìud8 \ No newline at end of file diff --git a/.git.orig/objects/23/b17f5fb882b1845b1dd3bd276509d086f658e9 b/.git.orig/objects/23/b17f5fb882b1845b1dd3bd276509d086f658e9 new file mode 100644 index 0000000000000000000000000000000000000000..7c873b697b53c7ee5cdf85ccb549399066ed8831 GIT binary patch literal 585 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 literal 0 HcmV?d00001 diff --git a/.git.orig/objects/40/8676a416aa889991156047b423a4def5f4625c b/.git.orig/objects/40/8676a416aa889991156047b423a4def5f4625c new file mode 100644 index 0000000000000000000000000000000000000000..2efba322a41acb815ecb7f9d6801a1c4defe193f GIT binary patch literal 5012 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 literal 0 HcmV?d00001 diff --git a/.git.orig/objects/4c/a02b3951a7edd86698d6becdbaf38a2e0f2e30 b/.git.orig/objects/4c/a02b3951a7edd86698d6becdbaf38a2e0f2e30 new file mode 100644 index 0000000000000000000000000000000000000000..08cf74f3ac7beb6f3fd49550a8fdc65e6019772e GIT binary patch literal 684 zcmV;d0#p5X0ku_4Z`&{o-8sL4NG=zdt)!clC-(Q1pHMCYTw zew5@EO%bd`v0Z{hA|Kx)AC*N#3;6iy%jb8xZpbp6kXh=o*V9Y5(Axyf`uztuI)b#o zH5AkX$U_9ArRIzpH zlgqQ|`S-J=F8VRXVna?d**?h5Vk#}^NL(rBJk7aBoqH{IwZ zwkP}s;3$|eb9cIdn||EZWQMs#cKzGeYk)>PwIN$P@b~ynI`=pIvOT2@zL1gXTFIu~ z;ZE!nm8H5E0`D4WLb}k#U+d!bb@-Rm$^9>l{&zjx=lRBF?t@7G{X6=$&JI9cUXs0# zw^If&d3=E%P5aEqd$q%^9vFV0^a60V=icpcIeq<*2+v9;r(t=+-JTuf|DZ%WgbLRv SE0AuKHS)xSH{uU*&o~e*SXwv$ literal 0 HcmV?d00001 diff --git a/.git.orig/objects/56/09cc7941235f99ae9a3ab723fa1c0855c62351 b/.git.orig/objects/56/09cc7941235f99ae9a3ab723fa1c0855c62351 new file mode 100644 index 0000000..f314635 --- /dev/null +++ b/.git.orig/objects/56/09cc7941235f99ae9a3ab723fa1c0855c62351 @@ -0,0 +1,2 @@ +x5ŽÍNÃ0„9û)|l¶×Ž¸¨Bx +B¶³ù©R’:K%xz".«Yí7C¡çÆÂÃ/˜ˆ7êØ¢ö6ÙhÛat¦ÂSp66¦n;_{è’aô½ Oóõ:£Â¸ƒÔ²Ø¡ï1ó×þ>Gœøù§ˆçm´†lñþÄu£]å¯j.À0–q°â?‰½íÈ÷Ã@´¬Jõ# _Q–PÖA,yÞ*oûÇz›FBUNKȨöO‘Sé$宎ü`ÀXNh}d¿V·I— \ No newline at end of file diff --git a/.git.orig/objects/56/1018539ac44bb4e74ab4ab3fae65d93bfde262 b/.git.orig/objects/56/1018539ac44bb4e74ab4ab3fae65d93bfde262 new file mode 100644 index 0000000000000000000000000000000000000000..59acbb3912f0214f67c7145ada091de32a4c8cf7 GIT binary patch literal 5172 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 literal 0 HcmV?d00001 diff --git a/.git.orig/objects/58/e3843da16dab7e0167bae41fb18d787fd78d1c b/.git.orig/objects/58/e3843da16dab7e0167bae41fb18d787fd78d1c new file mode 100644 index 0000000000000000000000000000000000000000..a76f2929070ce8025e37b1bf779e1bbe7ff6b3cd GIT binary patch literal 1829 zcmV+=2io{}0mWGVZ`(E&zn}M4a4HT>i&##&+i)|`;(|770;Jt7&fPCTAkY$RbCE@k zq!MRA`?v4&D2bvSxl67XE&(i?eBb$bkMC(#su}zKhl79qtq}8-D(X_OBfFgbR!S$* z4|4r)qka{+V}<&LvDylTwX|LQ221}cgw2iAPO6F}NrI(|rC`%v&LDsl+;LX&J7L&d z8IsI?GJGX&mAOtqN(?0AMi}b_9HfWotC7jmL%#u|n2s%A>=N|!$F-iY3AfA4dSmz7 z^;Xm*2<`u_WCuOK!4GZ7DC$z zqtCq;{aOeBlt$s`2pYwCrny^!d*kOGh12T_^0X0iVT>rUyp$RCCc$UMas~f*Swhal z3F|r?2xZtx7t$^3S@MvCIMUn*?!>O$m0XxFY#UXUGoD}XV$YW%zs?#VI8eKbt}sgP z+FOVUK05ITF-BP7Iyo%>uDX~Ml9zYC3mkce8OC(!GF}uJ(1Q?OP5^DS?_q?`m0Vvv z5TbHAaeVeC&P&Oyz;gSg-_ZySUx*27zw|p+)g~8NZAu_;=^WIL$IU^dxmt|@d_u1vur!L(7Ng9J*h}K)V;#I%c zhYlQFQp#Df&k{0cY-_ERaS7Qyz?f|?U6EU!sy(I7=+5K{6>6}yz)w;6lfO(poL(dW zYI9i<9zNgh!77%b)~&d6!lYtt8DQm~=f_9yJ{-SfXQywEKTeNddY>74dvf&g{qfn) z?_a`>yk4OM`Cx{EP|E5W!vB8XGBEaDx|4eLBp|iCO~9k$^>HA0ZwFz;Wd#i=7R5>g z9JVkMpI8-NH zi4#apfcoT9m(Qqi8vz=-+9`u1x8sFyPF9PNFXf^b-J$}HD5aXa!AlL%+m@oV%Msi@EROx% z^lUQf3(Grn8)wvPY?&M~jMiHigw$OaRco6eWwVE{yyy@XW0kSb=J^gJJ`X4h3IN*^ zb;~Xk?x_ciOLdz$^}L7pg-7E6rSlsRC7h>{vuz3mrn4g05INa2c^kgSnjQ3 zQFC}Mv>M(C=P(qE|2T=>Qc}6>(?6E`!9(FZLc)-k)5;lTHSTXZ0B$_C6s5*3t8K!Z zVx{1wV!p=GB5ryF3m~8&pt*uTxqSuQ`}T}lA>8xm+P_PG>~Dkn$?9(Hciy~a2PyvJ zy{4}lVEUC?9CAAZY_Q;f7$EM-Gw~;Uc>4#*T&__HFRc408w`hRVyl zan0}a@W%}y=>9ULZDY)<^m|-k695F`pARE6 zP@=VNZD_q@w@XnmX|tKExVhU4&PQHQ20goPcRu-vo=oW^M3n84SMhKRpkT*Y_T+%$ zb;Ms7bh7O)2|Nk@{|CRBcDIXjo0F3O*`!@zqmjLqnvEoTtE#zNq*H+^;w(1gqEbeT z{J6w0D)oJ8)=vG(46R{)3d4i1NWiUsk}_vx?!q?BGVzU=Sk3XW33ZRV%q|5C3AD{b zvGKd1HbR#i<2r<8e*B7A9tzB4m08?y`SL+~*{Tl{A)3JLCHR2OvuQtU)L@0~B`6>A zCCH0C{3?-=H}UB{2xCw81a)+$p%@Om;Yr#wr#Fv76kYe?M}9ic?Ke$-A^=u;raqkB znEs7d%-^kUDve%$#l&HGlNtzZUZusf?cye7_a5uA@y)*-!$sxo>loUfoncp-651DP Tu_nLd)3fW)sIvY6PsWM+V^W*{ literal 0 HcmV?d00001 diff --git a/.git.orig/objects/5c/f1b8583f408862afb161ae09fc7634a46f34a7 b/.git.orig/objects/5c/f1b8583f408862afb161ae09fc7634a46f34a7 new file mode 100644 index 0000000000000000000000000000000000000000..cb62dc7811712caafd7d58d0be23059640b291e0 GIT binary patch literal 154 zcmV;L0A>Gp0V^p=O;s>7HDE9_FfcPQQAo`zE-gxpFG) z?p3leYY@^0X|v<;>swJ0;K5~#i)AEYwgvqO`8b8d50c-`# zCoL4I9Bj+`^Udro6)$mpb#o>uFIhH6F=H?^FfcPQQP4}yNKMYxORXqonE23Vqxbpcf}!Va=ZMAZ zd}?pe`3|nCC^aP?tZHT3U(P+6+};~%`sC#7uYO*9{LfXGs6*IW3+0QSn zmY;bu^Ge!N#_aR=_n7^I+E`YcoS%})U{GSXp4oeGT%*}@t$kZLzs+@Hcm_4g*~8J# z-POn6T`xCKE+F;I~yO@Zx9Y3-xurd zPd-FBLe%ACCNb#CGqf+Qm?fIN&MftzeSiMps6~sRvia%x#SA6=Ss~7)Yok^2OE0~4 zvJDZtVig8;Yi?!*IJQ{Q|8nlW)UG3!CU5iMQ1Sa&?~2wTDapxC&Sr3ED_bJAs$=Fv z(FFG`%1iEj{gMQAY(Y_G8AE+{bpNcp{x-F1w`#8Z{}80cv;Z3XC8@#Ne`_E;~XAfo`3*Z6(<>&|lV{|)t literal 0 HcmV?d00001 diff --git a/.git.orig/objects/7a/06bde184c49014feb523e9a54b726df8680fc2 b/.git.orig/objects/7a/06bde184c49014feb523e9a54b726df8680fc2 new file mode 100644 index 0000000..09f5ef0 --- /dev/null +++ b/.git.orig/objects/7a/06bde184c49014feb523e9a54b726df8680fc2 @@ -0,0 +1,2 @@ +xNIÂ0 äœWøTqj· Bˆ¯àæ$®Z ZB¼žÂ¸Œ4›fÒr¿OÑ®UèRزÄN“’çÁjô)OŽÕRˆ98ó¢s…Ír1ó–hY:Ì4$îc`ò„âã3‹G#¯:.®’F¸È<é Žïœ¿B“u=öÈ­gßv°·d­I¿‹Uÿ*›´­êŠÞTž +«–ç´Ì°Úg>½¨Lj \ No newline at end of file diff --git a/.git.orig/objects/7a/dbb168debec28b54ccdaac3a082e52eb78bae7 b/.git.orig/objects/7a/dbb168debec28b54ccdaac3a082e52eb78bae7 new file mode 100644 index 0000000000000000000000000000000000000000..3e36d2976f9092ca74e99bb89a9f1a54eee00441 GIT binary patch literal 1847 zcmV-72gvw%0mWEtZ`(Ey-sk-aPQ}o)h~*?(H`G9j1#Q*@NZT#W_9X}eTB0p3vZ#?% z;w)%?`<|mDigx5KT`?>l9Gkp%K6kw1tWY!d?1vwpf0c>(Qe|}^*rCa%e-_e;_^n+1 zqSYTFwJcL#GgceHuokzAUt{T~Oqf*5%1TwTD2lLjo(nep^%MeF#w}+BzZIIzl_tsT zXU&)5M(L{!bOep!G^x1F!M*l#hr;P}d3lul-V>ry zI&pmVC(aAWjlgpIrQfL#8om${)_&=CjH-1il3Eu)U~VnckH^hH#i?430pwAObo$P^+I&DktH9ZN zVMkgoQG#4BeL*N>c?IF$-n9&jy^;2~o_!XOTHYq$!ST8{5WF{ou;j9Y1{8~IDLf8a z7^6gC@A}bQSGhlUib5@LJQ4jfW3xo98qxQ-Di@wctgPm6sS7w`cTp3ac$wi)t+WMB zAUX!>qYqs^qsFZTXl!e%G?Ltm7s6UuE=I1Di)?g*3OJ&aYVHOn)kkkrimGI4#(bt~ z8}N*8a&AY`1WOMQgNo!b-Hcrs7iB8U=kZmlxLqn;O)lX?3%;VY%Er|z6{;v4^rp(aP`)_fWL*qzR6|vX#ttRvA>?4 zPDXuUd5v!4l$wnVlYNHKdJ6-ex(lu9$|OkHKfy6@>=M4xpqZ74R3QGuX8 z??e!#rb}GQUVjAZ!u@jZ9_uZM&qSq$cl<#PMe{c+(VR-Ex1lFqS^U;v=`4WKkeHLw zYGo=k008SYQQnaFq?~Ut_El}HS{|(|t_Fx2_NXB`DKoJeCOD&}kE+G+_7r!5pv)d} z<@~nV9u3+YVmj(Gb({A;*QJY&iP5w#Nc*sHJ>99i8dQd03tgg86JW>j({XE2gR|f| zdc@9Z%n`UUSuTnS*K}nfW)&+0*ClgBnikP2AXoqaO%cr<2FlGl?9TOf)JvgJK->Ra z{Jg(T`b|tK-!%jnKMpo223h~$21nh_5_=&FH~B8ZN74==JB{MKc#p=Kz37R*&+fYE zrJ;wFYdkax?SqG*fU}n`Sob^Lf4(LJO=O}f(-?CQKg0d0%HkG>Izcr@TT6BPGSDhk z3947;@WH__!!FZxX(e?+a~zyl7vTE*PQ?EA!y*C}3>_I7?a29oSPV0KLZ@ryF~5C@ z45MbaAaA}z1b~_FI+OPEg*eCuPq)8WeySjSh{3JJI3v zIQjkPYX%?A9Or0L(2@uZ*?tP z&+J&KFI?vY0KxdrGY!q9=+IjmS})m6E=nd%GLt3Ow>#eXzzfQtM|bVc2lp^QrgRbl z%67@AxIax$u){3dh>j2A)_~(x<39{K+4i3Vo&^8@!Ec7<%@W<_SCC<`gS*I9}kosTEiITXAoDBfLr&UlvyoP>$hnn z3h%|pR2*+ssC(QdCKoWo(>8<2+V6&1i>lz5`XMZFQ(VY$Utl83#NeJw^Lt%ttKJWm zXyZ1Q-~$@irU@}Yo8@~LuYAZCFE8})i$n(Agr|Eij6L0FsG~a##c=4MC~DK39x#U} zx*o>89MFMoK3M&O09fgm`fxg9`iHZSzgt~b8oZ)~#D0058VGG(~dW~`z{x1a!W2fyUU9z>)$a4n*rJnh}r-E literal 0 HcmV?d00001 diff --git a/.git.orig/objects/85/0db890df9cb03de53e701189b775866254437e b/.git.orig/objects/85/0db890df9cb03de53e701189b775866254437e new file mode 100644 index 0000000000000000000000000000000000000000..1802e2f94a8a7a252a25294478bc391103daf206 GIT binary patch literal 550 zcmV+>0@?j|0V^p=O;s>6F=Q|_FfcPQQAjM#h%YY8$t+3LORZpdFf;6vfn0Ul!JS*` zr3`BfyzYxVg(^%gEiTE=jZdv8NzE(H%+CWUPddBmYS?*>jYk4~wNz75o-O{ossdGc zW?o8a1xTsqZca<-5d3Vjz<2T#nw&}X7-zj+mwJ|p_uQV|yz9=;(5v(>NvjAjuubO9Z+DoTWd4>&h zBqcuPyqOy1^YWLI+l1Sgx>LmX#lDMD+a5}nPw!Oq4&@x^a!gy=b8I4^Ad9*{w*m=%qvdI zFUn0VDrSiJxFf>ep(APi#>91;f67djXJ#Mqp3ur-d&hNO oK+f&$P?LZviW!W4sdGs@id(qPVCI&&Ctc3nTR#030MP6Me_NspZ~y=R literal 0 HcmV?d00001 diff --git a/.git.orig/objects/8f/d882434ce2289e9789684588c0ce48cb1c4657 b/.git.orig/objects/8f/d882434ce2289e9789684588c0ce48cb1c4657 new file mode 100644 index 0000000000000000000000000000000000000000..1eb3ffad4463060bc9c12d8be333de0437207616 GIT binary patch literal 5019 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 X-g{b__n*s}&mPP?7Qh7n(Ax#(b^bZ$ literal 0 HcmV?d00001 diff --git a/.git.orig/objects/b8/92bd45591435a61d4fc57b954841a8bfdd5a81 b/.git.orig/objects/b8/92bd45591435a61d4fc57b954841a8bfdd5a81 new file mode 100644 index 0000000..1e6d69d --- /dev/null +++ b/.git.orig/objects/b8/92bd45591435a61d4fc57b954841a8bfdd5a81 @@ -0,0 +1 @@ +x•ŽAnÃ0 sÖ+øeFRP=ô½Ñ$H¬ÂU{èëëù@. ì,Xm×ë¹ ´ë«;Xå˜ý :Š*q©Ìcf²C&Œƒ•‰(…/Y}ÙD”¤©Òfɶ$G”Ì–G-^'f™Œ’ùés[áSt†YÎ~׿­¼ßÂî`oþû1G¦ˆX*¼à€ô~±ûrÄüƒ5ý>‚˜Î²œüÒNÐÜpød;Ly \ No newline at end of file diff --git a/.git.orig/objects/b8/c859533235b2d9acf2541de0619342983e2192 b/.git.orig/objects/b8/c859533235b2d9acf2541de0619342983e2192 new file mode 100644 index 0000000000000000000000000000000000000000..5802440e7ad5b1fb26a7f3d236f806d03e059bd5 GIT binary patch literal 585 zcmV-P0=E5l0V^p=O;s>6F=H?^FfcPQQP4}yNKMYxORXqonE23Vqxbpcf}!Va=ZMAZ zd}?pe`3|nCC^aP?tZHT3U(P+6+};~%`sC#7uYO*9{LfXGs6*IW3+0QSn zmY;bu^Ge!N#_aR=_n7^I+E`YcoS%})U{GSXp4oeGT%*}@t$kZLzs+@Hcm_4g*~8J# z-POn6T`xCKE+F;I~yO@Zx9Y3-xurd zPd-FBLe%ACCNb#CGqf+Qm?fIN&MftzeSiMps6~sRvia%x#SA6=Ss~7)Yok^2OE0~4 zvJDZtVig8;Yi?!*IJQ{Q|8nlW)UG3!CU5iMQ1Sa&?~2wTDapxC&SvPp(d6v&NMqjg z&J5R%1Lr(W%eaL@tSKnUEMutej_#k8*Wadg?N-f|{~v6WiT`_Ff%bxFfuSQF)%SOH8M6aGBk|O%}g&!EJ=+oN-fSW oElN%;Hq=Y4C}yxJy?a09WbnPUK6&aZcU+peLHI}^03)j!m-_P@*8l(j literal 0 HcmV?d00001 diff --git a/.git.orig/objects/be/e6119cdeea9728ec3144ccbb18bc1d8a239deb b/.git.orig/objects/be/e6119cdeea9728ec3144ccbb18bc1d8a239deb new file mode 100644 index 0000000000000000000000000000000000000000..21aa1dacf14ce5889ae4c830dc87b1ebb070e480 GIT binary patch literal 79 zcmV-V0I>gf0V^p=O;s>6VlXr?Ff%bxNKY*($;?aFEiOqcDoIVzEzc~;&`m7P&@C>^ l$t+3L%S~bUÚ­Q$ðÞϵÁWåN•–ºòÞ/üÄR°ó:î ¼²V†1x“ZJA¿ÂÎϼb)‰¿¡ȸ”ãŸèAá]âí¶?B¼ç ðv†¹Õ zƒ<4Œ…^ÿ¥ƒÝ«–{ñÓ…\g \ No newline at end of file diff --git a/.git.orig/objects/c4/cb735515e9ed67db62710b6998d8eb9af776db b/.git.orig/objects/c4/cb735515e9ed67db62710b6998d8eb9af776db new file mode 100644 index 0000000000000000000000000000000000000000..9a3b0e5298e5b971dc06d9e76970796782ed90d5 GIT binary patch literal 55 zcmV-70LcG%0V^p=O;s?qU@$Z=Ff%bxNY2ko%S_ixtte*jS)gqhxcu#nv>Dg-o!#}h NOOIdA003ud5xsQZ86yAy literal 0 HcmV?d00001 diff --git a/.git.orig/objects/c6/78d7bbcbfe040f850a951876de535b837b13f9 b/.git.orig/objects/c6/78d7bbcbfe040f850a951876de535b837b13f9 new file mode 100644 index 0000000000000000000000000000000000000000..7490590615c065307369c13be1dc237d0020948a GIT binary patch literal 5183 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óE{×iûC·ùì|ôD AÄÝà +°”ßGÓþã.ïݺõGwËëÔëå#+Vl \ No newline at end of file diff --git a/.git.orig/objects/f6/4965c0271738423a6d10c10be61b2245b628d7 b/.git.orig/objects/f6/4965c0271738423a6d10c10be61b2245b628d7 new file mode 100644 index 0000000000000000000000000000000000000000..2e38ebe8811290123438b46f029aa5ca7f8d541a GIT binary patch literal 53 zcmV-50LuS(0V^p=O;s>9XD~4U0)^bn^rFO)%>2A!hTR{pMJ<}VWPa>|h65`_rz|;i L{@#87d2$p%S_&6M literal 0 HcmV?d00001 diff --git a/.git.orig/objects/f6/69686b38ada693234a078c22481a3774e36cf2 b/.git.orig/objects/f6/69686b38ada693234a078c22481a3774e36cf2 new file mode 100644 index 0000000000000000000000000000000000000000..05e81c95462eef4bc7192678edb673f2f5ef6be3 GIT binary patch literal 12812 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 literal 0 HcmV?d00001 diff --git a/.git.orig/objects/fc/ad7b900f8a691fc0730c61b5befd38aee71e36 b/.git.orig/objects/fc/ad7b900f8a691fc0730c61b5befd38aee71e36 new file mode 100644 index 0000000000000000000000000000000000000000..beb57480d16569442eb8c3e65a2eff46df626d1c GIT binary patch literal 1828 zcmV+<2iy2~0mWEtZ`(Ey-sk=bPQ}o)h~=hTH_Skb1#Q{{NZT#W_9X}eTB0p3vZ#?% z;w)%?`<|mDigx5ST~RCnVu!qUK6kw1tWY!d?5C$cf0v2*Qe|}^*uKf9e;3k<_^n+1 zrqy2}wJcNj7^{t7Sc}`mdszB46DHNNvQm{SiXtqX=7LRsKZXF7am!i3uZ3oFrAab- zt@%=1DSZ)xlo&|JrO?I+cpUG=Puv>pVO6M{u``g;53i~Tn{bmS#@V^suD4PqUS78$ z3b~Sc!lI8a*j~K17j+xPa&41J=p`>jX(!G*QC{vmWY3?o3G`g4yu3<_xzJiACH@Zic_^51IVKm>GZt?wZ~|# zmZE~>|12QIVW>hP7exX+R;7wSn$Q7sh;VyLG=%PZ- zqFok|Ib%~-mC`mMy9XGPHKucN%Tcwb)EV8GT%tG)))x3FN_X;S(c7cb$U|)|3&O+4 zs~uRyVwAZhw^ry_tPBIJ+~>)`{;RhKkJ$0i%Y%2*gGbJ1#$F!ozk73V{Qk`&*pb#t zlpq&OUl0meUO@N{H!TBWZ=^k}XI})QmbVFbaJ()K1nh1s?G-8(D5H z?qo(NgE9+2y^Cs|U2=Gujl~Mrm!WI?PU<`#uHKp#@V9W-H@U1nEg&;E_E*#6$*3B{2>yKbvxL@wwW4$HuiKx`@jz7quX#Qa(`cg^tHuS_Ri{Cmdodi%C5_42qtxSam z0ASrF${P|NmGdpezO0Q^%Y&80)c{e$9yLTqWhPd`1Siz=QMDM}p5jgrl-WbBoL*Pk zqd}WPOb30YZu9A$#)t4B<%>Y({MkI9{MZnrW;rqLKwD2 zjKOD(82w?QD${6t5RtXAIL;pB_@?@A{m+88+lA4@3 z&P1#Wa4mc%V(-Uc5djP4i3~k-9s*_$LL{b+S`ftr-#X(2Pe~`_wOQ4$yg~1HhSSx zc|#=iwDg0&yOoKa-#We zZD_q@SGg#eG|5bsTwm{a=L0V&Meg6UJ0IN4Po{Jd0?KyDskl8TP_V;n+K7%1PO7ooxFifhWQLfAE`mc(eMpIXUr=joKA98kq}Ov5{miRXLZ7cq&jutigs{luC<{ z8>kqDslJm<+IIn^ht@D*`SHS4B;eM4k}|7hYW+4%JK>cVnTq2j3U!a$#N+~oc-m&> zSo^=B)}ksn26_lf+<+Fc+!mO~GBLQq()?Bf+N$?6BRaIrCHR2ewCOKQ&_Vh3#48{2 z#mfsl{3?-wH{t2t3S&?A1?uQdLopnB1B%)-r)S9_imp9zj{7Fkmn-00M=S;+$dz_ZzoAFy-Vv*w_0v@Z+-ZYfT>#uR^3t z@(VJPiy68bmx+bUIMP_U_vvhbDN4`&Ox)A~RZv=zUzC{$G-Tg1!8!L{P1ks1=yGPa P#2(o$<+-l`)e$gzbX7B) literal 0 HcmV?d00001 diff --git a/.git.orig/objects/pack/pack-011e412ea3833a4bbedf2c966c4755fc019c8240.idx b/.git.orig/objects/pack/pack-011e412ea3833a4bbedf2c966c4755fc019c8240.idx new file mode 100644 index 0000000000000000000000000000000000000000..f9fd7f7493b21ac1f547286c126f2addf37f7cc5 GIT binary patch literal 22380 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�8Z$PR~T7Z0}Dyc{*a5u#Elm-@mdKOipe)Jpj)e@ zKaqtCbr`JAEH@IzWxT&&nC<&woyvB!fRCxzn*YNP(6iT87zB`VAjuR60cF_VB`zP(qq=b(h_hYNZ9w5n@tZR`J-c8mh=YY#>GORI zGY5E2^Bl*GW;2nzyXk1+-7rV10Bi94hBv9(K=Tk94aKQ*mEiI1r z7uf&t<>9GXpP@mD70S%39NHP;)BGdoA>=OBR1bN;yj9!JpB~c_XSl&>nu$MR*!0rq zMT0|wk`xd!jJ_j^ejer&p=<|P>N9cXs}tRqt4#ueSA@wHCrL%1khsiAJ{*~+(lcab zgCi#U#;bXpqy?7{()NoBhA%7#Tvs7ZTfd08vlwe9NNk76@e5O-wQXR;Fz(ct3j>qW ztAmb2u36pa(fK@-myhcRNL2y#*`W4hLKReO|6{H1zhXF^C$~qMU>4`X+?|r}+8_ zR6eaXs~N~`Fh}6Oq8wAbsrYsx9zPb}%34T`6dCyK1J$buaOk$gV?(Ln1$f;!xe6oO z+?`!fwQHaS>UFYbDFncqypEe(;0xt8U1#7!^r6BlM>wGlbwl8F0EOETcood^Pv40@ z!SC5$>jGx){{3(6&+?D$Xc80vakhY&UyPksgzL6$y2#@O8LiDDZ;+=6x&`_VuzTSO4DaA9(*T8IAv%D{YE_6 zCoq9nU_ZiHG1#maCUF|t>%1(-li3=y!nzwf)5>#8eqZ09<+SC|80)Pg51>f}YyiC~ z(%L@}V0WHe(+oMk&{&h)Xe z?!039h{uT|m{P~pe8@*o`ES9ucN@Tu?ypWU#qTO_#?{0r0*Ys-!`EPV+M;l zSrjb_;Ri6w532lQ_W;K0Hz1UmIWoZq$`vOF#D}cJ1ly~+LlUj_Bd4;Bt~$~r6wSC$ zbOqqgG!-c6EPnGFaW;C;jF2;!{DK*NP{BjMg|wvr73Y(n`8=<1czu1y3KB@B1LpMt z##Vi4y%Py=#&r-F=bQDH?p?-+ScjjhA-g(!DBSp;ggl5%~af)6-yRGJFj@h3%&=4?yv`zhT zmz(l^Dlw*tMOp*jqn^K?igfQZ<+nv9S2pj4`orw_x&Haug>8sWw z_ls2yI&VlaD`=3e0Vg>9q~T`YtH|9VIhd?C7 znsRY5)b;^Z1Z%SclfFAB8g#+IA!8%M@TC3(1(pFZ!uV%tS5!r5>+F&mbUVG-pTtVj z4=8VyfFfR56RneR1AZ3B0A3>zKS}P}TN(|(k}Zydrh9b^1@Bn-PoJbC-`fFb@Oce3 z{Kt-oFslMn1+pq*0BF0c>C#JZOo1*jBBBbMCARK4M!$KnH0#b0qKBUDm7PE%99JO> zSqnIGTMX2=Zc3@`V%Y;2;pv>aR(E&IWAPj0zM+uz=G~oNXofG~y~H9=&QRA=BlY*z zJ=NcwP)rXj#*HE}J7HGJ?Efq5%#@#Q&}9Mv0dn;VLj(E&+fes&`9cd~l76O)8mIg@ zNY7HAMEOjP%`JC#C%+^Zm`>&QJg3$zNX5&JW^e(Mzi!3T z8qlAV${fN1I6PL8$_dato5!ss5ZihF5#(XZOYL<$v0cL?r5I=#^EU2{?>(Bm$4xU} znI^Kn3QTFHJ4iGNG%FqkXhJb$z=e`QNZ@g%bIX}0oZY@-F5iq+S{M0P+iHgk(yqqz za_bfSW44~HH{Ce7fl<~sE$-=Nu`E8p+N+d!m?${}lqF$6NwA;97}p)jkpnq;7`=mt zwi{H9 zqxQRvohbs9yT;6XP>|8$pR?E5xr=9*eM{qBJaDnCvK1|gF^5*o)?`*!xs?O!I<41T zJ0|%}|1+X0ATB8|aXzK+!P#ubatK_eM2K)Il)Q3f_4?S>Ontt31C%{DQd5wI2&D&y zE62z=LW^h(lcst#X$Iv=k&v~XFZN6Z>iKlzzIS&jv~_nUD&Zox1H<3s<5(C#RQ5#9=&z?`F_v+*%90xBRD(C?;rKwa?Dd6bdJylHU zrZcI+&V{#{p%oi&nj-O_#wUS3Y7%d6hVRademviwIL$j&y%~`A?2}N9CgKk=J!vL) zZj7BSF&={5c&dYs0@u$PB;&}7tmxVfY<&NBMsdnGk zmB*{5-mGoLBUQU`b|A<=Cy-x=(=q(of=mfcK6OV_HM@q@f2V)Qz z-%iEz)9;`D`10-KLbXRPaUqlvEGQ{LuSHV{i6+ftB#a1hBaJZa6CHXB_QYKckVa^F zb6vnJN}Y~pm7x{70}*2}KuM}(na}4;;m2)WHEm}J1^Qjf#Hq;jkE%42nWexNbKt|) zOH7Kerz}r7vq)FHI)#{#i{V3%oT*jKC!P&*hmJ%dM7JJ|$Q4sO>jK>pu_#(_+UD2# z+!~#&Y%}ST1{($#r8=iVuIkDg> zb4w|h5vve-+0GNWPBk1kx=qMPe&iIT7{7A?urFAve9&69@YZDRKb^SZHAfb-4m(Kf z&ZG-UX0<}N8);CVcw~U%Df&kFTd`MqdSmY#??{plUtV66THd1c{Clj<`!NWise77f4Q$qj1%XZa`&Z zJMvZLr}v4X*|7^SKt1%3SsQ)Ba~90KAr1$5;xKx{Q$GM76r2EckF$nOg@-?qIwnF9 zN%%vh3o6uOgdI{@$2BnXTCs`@p0r*yuFBBt0GmE=r-P<3aZWck=!2@7F6jn-7kp%+ z0bdhl|5mK!8ieZ7717JegvIRclZc4$qN>28DcDyT(oa+(>sF)hcn(*}PQUd(1|m(c zGgfhC5BzjLli(RfsP?{N?tW82tO&)(X;Ze6GyQz>J(J4kBBzZ$7J@=C?zu(e$c`xP z_Sz|yF8?!w;3~ff-TauBY$3FB=DE!bJT_lbPtsX_r<5>8eih$^^w67el(H5Nx4AKU$SbpJ-VBr`LLS0w5IOeMt~#U{6)!lk7P~Sm zjw_{f3g5saX+9!z1-61%-=2=b#YHswlhlw9#Q5{tSD7gewQCV5sSx+4Eru zv_609sW-L;tZbq{hw{CV>SK;i2zB1**{i27Pv1N{9c$SzdomSICZmX6RWm#O$>!&~ zGZU%vlfsGr##6PEVQa?0h(WWbmd#J8+B_?*7pVtP8X(l5b7XNgnjS`vGiEbl&yMJe z#RXwFBTNh-avg1!O3XuT*HVCk@0LL|Bq{wXcjTU$bzQ5JtT?W-@pDfv9b%L{(V)6< zNDg}F|LobKSUmoCW~!f0y(J-KNc+d!z()qV5;i9J#3|&NVkY#iKKqGp+nq^U_1rV9 zq{-G2js6l-jN2#5qLRD9$EMC*B41;T?d1DJ?H*^zx2A^CGkSyhJ?^xB^C(!LcYOMt z;=!HT_B{o4;`C!<0SYK^g+Jhy)2HTqnEc~BCvC3DjoM6v!<(x8M1L{cm7C%N3T5mz zq0Ub<@o=C$i&0LgV<}42!dAeZh?s{f9oOO{Ax2k` zkwvb!6>D<4+o_19^!!3FRP`7rwwb^QKQeP&73E12W-LoO+F)wncLcH)6s9y4)wt2~ zMGJJ?wF!{Jg8qTg?T;!tCfmT6`TlO~1PzLb-(PAd%+IF!E9N!Xf--N!SSJh(+T6I# zTEyuS=GmAiv8l+0u3&MA+(03bXU77Cup}QIW(rHGJjf9oAx$!cS>s>%VkmTc{H4#(Ule{QJou)Vkp)7rpwCwwCb8IX}z6+N+*@WUoD1qdqd57)u-hL$lzM& z{5^o_M4eRxi#_p$wNY3D0Ec>+Ab%}!VO6qjJ7LR8KFr3@58?HIbs+V1bPU?N%p8A~ zwVgyNu8q;kv+2%s6efiog#_7yGEKgxVf_dC#S9bVCxaz5Q@f#6kBQkn@#|rM6WK?( z(k2nXtyNZUSLW!6t*zK?fo#LiL$i4JZ_#s{m?OZj_Y;6yc=_QhW>be z>&k+=CnHnf62*JJyOZbg;{P3w4j%-@nwoB0y9a}VA7iK9FaT*zCOYR@I&LSXX*E3_ zG<2|<1>$r3FkxIVH>)>r#tadim_hnpV>C}rdZ1j4r>-iG@6u8AhL)Lms{t&>BHa3Y zSe_k+++X98-j>Z{S(A6jk^ie?c?>vs@g}Em^dwYl?j>GQz2mqi6yW2~NI z`n*THm=%x9xjnYuk+XXArf+**XD_~St>B*q+rEN6VQ9DT^`hT8sFBc3z0T!PE1tKi zVZBAdwC@Sn;i^Ok18cCP9M6F;Y_5dxaAszo&tnoXSR98eW4(NC(4wt%DjwarrrXj{ zu0wg?Vfch$yf;b32|7gA$!&R7HaMmS6We6C!il3g`?qZw`IZE}Us)6_>vApH0Q`GWH}2@yZ8$1BO`nzuoUi2Z5X z*7HK13;sjhy{xlEzSsqcXgZYG+qjJ@5`tpgJDG-ZP5bt}_~a7X_Gjelt@)vsh;@7~ zO2Yc(XArQe8j!DU@(UC1KY}>rO!I8g&mvbL&-oaj(P5JIieg?2Y5g=!B6Johjb|l- z3NF!B@$0a6UQKU%Rg~<7M?vhRv*>yRdpHf;mJc)(eO#~}h|4n5A@DAaHk$iSAomP9 zMTM->B|l3%nl49F$NYbuT0jdUfsZNthfo2<#ec*Cp^%ucz=WlD^?*3;grj7PAV7!qwlAxPvN zfudDC$UGG~!?~lhFG`c5BQ_ss`llnCD+$I;jX=zC*MTUyuUmcsIKPMeNS7ie41=XB zc0a0HLM4~+M0w@m0c&uy$%W=2$+DWLGbf4bY`82zi_W}OWQOmyv)WX0zwI75BcKq8#=S zHi?bwh&AmmV#@>a))%>Y;frRUtwJxcl$gM$%(hfwyq9f^g*l zb*UqzJ=mhf3GQa~l4k6>8XL$s)33>;@S<%3MVOy!=pqG0m(#>v_A|2b*4uJQo~-r< z?I;ry>hv7Jv*;9?KGhR`oJ5fB%9?P>ad*ojKSdsK$}^o~fZ5&UWS6IoB$Hb#C>){~ z+PxDem)IOod5ymwtjjV!?s>&y5N})gGU%t~WE8&-`|cv_EifNmm&UOl>|pzRaehdk zGX%MV(RSW3%lbpdUCEVC^FXbN_?h;4`{J+|2*_urr$^TR?RR37uF;^`0~V;~VZ!kV z#lr~a0(FZ*pQdYXrRoL=&(0IwZQZN!+RsyYpWF9wnlDIzmw9WkEb^(mLF?T+jC-27 zrDu=jpM$$|2}qSx`Egwq^P(%aY|BuXWNJ2V#WEjpD^N7g*KQ@jO-oP+n;6>n6j^mr z8PMmktx&_j|2nDl>L7;CO}b~>s1rg*?5$pkc7Fb!$=&yNnp=d+OXrD^z-lxDBW@TxfH!!V4htyRudD$lYCeQJ!5$(c~s{pu#v*%)*fhI1h)6{|NGZEI^Z zXfdvK;X!g?HXX`4veYG4GX{XVCW{DC^=L=-Hx=;?b2d zOIFj6q=?g3?@rKguj;LwfPvI_H$@6VdM*%?AYER&dwAYYF}J|FZZ@#WJnP0~_P1@i zQpdB{vb{&;@M=|E9j@x+8h8+KIUXSJB!&fGYF#~DZYn^>BodC@shtEA4{*`7_hCA2 zBnQAV5!bJvLux2hw_>Qap5Q@oG2lb!<69VXJ%1fd9(!pr&D_Dd)|a`^QZG&F2V?yu zJo{3gq(64gl{40@ZVES1j8_wi(CeQ|^OM#fw#_}it2-oTDORW>-tOahLOK8~8; zrL9B}PsVuU2>!dD_A(%|ANq=fnkHh9FAnWU?b)E@ZY;H$#n}#IHkjUiQz=tceJ4kl z{V$4uoU7Sxb;y=EW82yCA8h2B;Q0Z!JS@{cyDK<@{OBtJMEpFXW9|bG5hujFJ8(ex zPx?S~FBWjCgSvVSVu~HkWdV&I&=u~kbKkqfqV~A?g&besJ)J9dE|>HuT#Dxh1oa-w z6+gC`xl;P%%fP#eY=CscBx~6j8b3$NlyML7J3!-JH_plC^3!vYlVx5lhwy+V7xD4q z*uvuu4Cu~t2Q;EX1)qQV!b3V`F-(JT^YHEG*o{?wJxkb0ABp3~?7i}7+b=rtvUzro zdVE~nIpsn<4tk6VXZulNT5ivb#y*Ctn8kd3c+?@1nw$g^VUI-WJp*F!?7EoOZsT?h zzi-pYhl=iL2+#iYw=}p1^9(Chb)6Vv4ZVv{4Va(u)~jca*#53|RmRj`bay38;$hxZ z=UW?u9Vxf`v+qwFp@D@h(q&%5*I7v&ui;R)JOqIJnm5z16YauVeDsXyn_YBLbbT`wKiL#0 zWumAFc1Dc%yYpMhHWfy+Q@lRoj8enS^%33I@|{^2IFm3?V}Z6FiV> zQIK&bA;xYJp^C{1OYy8vG?2Z+5_uU45);UdCKokKn%u4ycL} z%k~PBP^S0;@b2D2w7;+Fn<|cc=as?u{}LoRz$sLI0knj7JgBSc8ApOnTphj;)V-hD zTzNmH+9z&su|GZ*WQHBu(RlO?r5*@_m(18Lg`cr)Gu%AntyVsxgi~CsT;zzIqRLfn z2cDRDXJ>lf<5d)Qc16{$bG~J6KhtvRPqu5e(5bGakPDu;XnA3Ar%`vI50b!*0qdsz zP%QF=Ue0`z%L@apHt-aISV#LoaOo(|0?^Q`GQJ|Jhs(Fn{0eyRIt!K`d;G$fL2_4L zqv~F{iodXSLLTFuUm8s(fDe(N%M-_fb{rVLE)?l)8D2Gu|NRegQ^>F70?ui8oICV{ z>C6+R4Ib)ZMXAO4rA5i9#rg&LIhDEjMFkm|$-0|E)ZcXh05_=+BjF1&c$_=)glYFP zrVYR6s474~jh#Y4c4}pOT4qj3YLP~AMq*xiYH@0bUP@v~;^v=oq*x_VQqu}h6cv`H z7FFsfOukSh%K{Rg9G@o1>6=)fSCW{Wsxf&(iri#{Ih@v-TyQJElKJsDnZ+d~=Cdh17S)?HV0CPS)y0rTQc%0=LYj4{|^1FYd?;Mft}5%lclN=3kV{HxiuQ0d982WR%+7me zXR{#7qFS^$K`+?)r%yR;NDLpZicL-mQZ~4uRf!hW8A9Jmz(tjzCC(XvS1s%CVi)5K z7HjgUr8Pk}SyRP7wxr(jx8G6#L`~gt`~_K}0+%h$)0*T0ldb6{UAAS$A0pT}K?!E- zlnLI_4;3wsCd1nMPki4AdL4+DCTr?V#d_E?TEhF87lHh;sRQ^fe+1!3KR(OpN3z59 zs^w~83A%&t7ZHNLm-7gb;+`xPWO1FKO-&Z5e%>1r_q1Hla>dLW4P{>8EHe+yc0<%d zs6HRpq(qz-L`!sM%>KO+lf}2N7$M)vE50rA|rw z$Amz~U%=6W_fi}D<1*~*Lu71CV=imF;%*eoZeRa=^Cq|sXOhlE7@^Tc*hKGAE|hRf z;Uwq_AxiufzQ^BHRZeheMA#+O$H?eG>i?|AtpxIL%;^X3E$sLFO&Ho_fE~@$;ZWk7 z$~`)z$NwhH54c(HG!Q~|tXU{;$a04pu=0J|kOV#3ac&Z!h5bH4HC?Tnxh4Z{hE}9W zH$ePzBC({oFAyn9aZ?pSMj#D@eFxVxzF{1iy<`_`l)l*#N;+UfT*m8HZx&_|>_VMT4T9*3f&9$8XHnA9gtLh8DzC-@S+BbXB`a4!yJ zSHz#sMxdledi+~$t6Wq&G}oXg8B$FGCaWM8v{QefVRn#$(jz&57-2G{82>)YEGZ*S0lU%!0u(~YRaoB^{l*Y7EO zAK)|`i66gJlu9!UoxHGvRq{8u#s$wC^jEUzs=wR^%mJFf6!Plc<{k2q0gDA=7HD6K zR)_RCdei}IH%>>}Ut@O-kx0(AP@QjUUp?K)1{N8H7QW9(P!m?Qb!H4>y32pz2H)Z> zspD7WvWk1~6O>uDCIt@C0c}}>#?WfAsWklvr5_>79A?YGn&PTx^M+S9LdGIVpP-rEHnvObzTrzm)HCh**o$KuGNMbo~51*~#E;qn7E zO%p90$64m3$murD&v}EB<8l=bw5v3e$d3z0or4T9(4q{7y^lq!&C)zh)&aLsXF*ZH zjt@9*3DaD?tQMfRlC?E~I4CwW2MGYs+bfOBEJzEHLDA2h-U*gsxxa2x4$*;wmX{2F zAB;9TcL^@Hb4xD>J$-|2%C?BX?!i(IRLbyF@g@PJn>DG6i2`GsiZo}11_Qx*2nV%>4%d9Op1{r;6y4}?zlEGs8} zyLT01;(U;&toU;D^(A_{CN+WMKX}WUpN`NYfyij{g_zvsTi60|5RLFNK~)Lw8Q!89 z0Dmm2M<^t?QL1Uygb9K(SzZ7HLLjFZZ2*fyD^jV`Aa<@S4H6+xH3txdQY)@s_%_Gb z)^1B!`be8~s@&>IZu=|Ucztt#&>0WBA?F#K3sa_D0_ZH8r_1%v3bCQz+Tr1LkWvv5K_uJsyaX}@GCZI$G1@BaoAf&SnDA1keI3p>Ka+UH0 zZ;l(=QaK1tXD9LWBwQ?wlHX5}6*(u8smC0}WhOX;q(X*2?0d|JI68uaD~NcS!m1z) z;9!%wXyR=T&F4(6U-T_^K1BKFMc*Vt7&9qBJK-Sekb2z6*Qw-*bF$s@b3jd&9ev&S zi1;^Ki6)clTbW{hQX8noAL0Z2-P0>M9TBgfAqN!Qa|+E9ElqvhNit7JvIEQy#`JPO zJ0bruebQ*B1g+7KU!Aju`FsM{BzP6#M!-P^opzM|3F+k7FqljODL>h3M{z^Z!--=5 z*$&Xq6LPlG4@lO=?rLH`$Rp!~W#&`Ext*;|)n!WnP05DPf<9;hLJS+`AsoX!)GPx{ zp&CiI2kl|hM)hv_pqPBntENg=DZ7KxYu!<%ha&T^1ithvLtLrVi{WajmY!=~>!VB}=kbaxWRT^V}@-M&e;JL;BU`0kzEd0A77W)1#6`$s%OU#t4eujS;$wzbW9N`@&iMea%XemCHi^NfhjZW619zr3 zYHBP8$+`$CRypcXV@7|up6K#yI`&ZKYJBo(PURj;^cgwOWI7{J54Drc!Wo6mp@!4r zU!&{(jnI)d)m^NQwT8|Vz%+u+f8Y3XcD6rHh+2`0t)+1ra@#pq>e~MN-*1eN@>S*m zkpXy|+v0!Ae}f+*Yi3SncIxC_KE=&`jA?2BXYU9cv+@FX0i{^mQrk!xzMrS)Rcbee z!ZI+s7vow|LlGyT3NjPS&J9Ir8mVn}kky**7KdTTv+vhONk)Xth6`+W|L61H1zW5| z(UgoFs@43Dl53Wp^1EN9_{?%m3h@IWjba36sXzPyM?V)#<&xK$i;5&kf}=yCEIKKu zrlh3zOp=9=8zyq)VpAEZ5?-@^~Pjnz|NJ?+{uQq+lgmQVlRI z2qn3w7JS)AR)AJ5Qh*!2`1l#*l$B#Q-Anh!GEaByMZp%N?kYma1>nset?Mb7QnkvQ z&^!LzlbS}N;ewU?j>{=YzPurO>E2!vE|jKHXEl>+TCqw`t)*CXGuk2t2V@HVMF1^W zt)|ecR->)P%omWBIZK zohb~f0}q%oUZqQ}SIt%O6vSYpxn%IkbGK{0lwY})qAagye*GMJzGC@x=7_+d>N&VV ziu$>`C99yLX8!{j#3a*!jt_v#V3UH=^8QZ-mxq`kq%L(viy}kxz=Ri5WQf?YVTAu{ zes}qVh{%n{@xS#rEjd*T$No!uQ^PdKiz)G6+8dWUStCnC;7V(-pG=&i(p;=32;{Mk zbo1Q@b&S!KShE_G|8D|FJ|#+y%w`$*XpsvOL^1rx-w^61KCA(Z8CX@A+~kW6kjV!F zs?sn$%U>`oDcmHj-M9lkxCtesPHXib<#V>>yqZQHhO+fGhw z+qP}nwr$(aoNwkY%vIlXRafoZz0v)=YuO|w9u}^TzFTizNRQ2j4#wV^k@a@%hG73~ z6oqUUi2}_(T4o9R{q841c*wbgpI&AIO&E0nyKP(5xRjNO0Dun>_Xv<~X(ih{AM9A8 zc!wPXPsqA(xi|L!Bk=7e=q+d{ZtbFWq>P+bWzSKW=G0mAXex_C@DLfX>q5`4da4Be zg|-Oad?P~W5=_8Qi%M|TA&#+3H8|aYd~rdibHOvOA*oF%7lgQ+N8@NmH%m>#t;`dC z(YUirLKxcObJTQ-KKf0RdGYAu5QYP4r^ULsnVQ+h>&c&`HHAnGi&Jj`3RDylLt6mH8+0oqz@ z#FRd;_KVb~K6D6OsBo^Q9Mcn4($!4}>9Q=uI5X}Xiqf-*pJR#(y`Xrj)@(-H_vHPV^}K4L2w z0XzIkG=&M!^Y+K_iixSGK^YWlUQCIxY19hE7%)nQJ3Sy|WZIs!Az zqCy%6Iv|5R$CtCeZG(@fWj70JugM1U_FLZG@)Vg#6GS&;hJGeK+gUfnGt%J5gnTr; z8^-;_j2Mqzl=G1N6>Sha-K}(9CW_J<*cqz3-(m2(iD|oc*xkplEWmhCzw-Bwl(KR;j2gCz(MWQ z9m3m^!3){NV%QGww~zyhkFC2CD}`oTNt30C+o;%P+;aGmfN?ZAMQ|CNbbtC(Ttr~u z(DFX2PApoGBR9Cu9CX+b^^^?US^LkGE&ARRHA2;dXZ^;(EQ4O&k*e(y)66x`m!MB;4y5THTg1A>fNgz1#>OWTaQ|%+a|VHf?{9-39KuXV zCnhGkJ=7I4eYOF9!`s|JVoq{(Ip`|m@l0SKlDw?1GSE%#sf|bnCKm|wk-?(p9u1vh&2Zr#t zAmuJ=GbXH8odi=6Mql$^4jRwH%U}%FdE$j7>WUE_1fd6@o7Dyb*UpAAQ>1evLID4_ zhZied`nt)#7+onx{~WIaDz3v+l(%9E?=y4O0l&+Ez5PB70{Ot_2Go8S{-1$$dRJ^# ze(;gyWYCXq$c(s^R=SA{WMVJLRM|`*4XSN>lak`BEmVa49_-<@MCrcg2##kkTt-(j ztQ7j3pB8BboU<-ntWNr^*Tk+rlGh`N<$nV45m5!P?r!YBquJUMPNs~#Ox0#e@pS?) zY8itCGinFo>@h0x&G1_FqsB7D=JIiTIlF0C$H%Yv;r#Cq za&AJkdnZx>{#5`ZGN5Q@G@kBR(Tbgtzyc^D#A6}O0q{-eticG4H5QAY(U3^F5(Htx zY3<{1>DwA+H>6X*?9!}6ctAyC zCVK@if0a6)G)38m9XxSGLN7Ut!4^58nxd4E6DmNV0@||7euK~?=Ue$4|7e5#}lwbG$m4X!lFGEXPGhaN2u z6cF{OeXKpXvuOI7=5BntRkiJ5bToFpn7Y(xaMUK66D0$%vd3%Wc-4pm%h~^Qh$GD< zRc|2?s6lR$Tdi_ya6B5Apc!2~b2O&BgS~ECKDIHrXJyCB*e%f`@ZbLY34CdYtYceG zkqVBRAOt}YPxh#yJa93^X^qL=NG$$-jbhV+pe#K>nNHp9(&%eh>?ib!RSlE6B@kZV ziq(&~he7e@Of?X{hlA~>8x`RSV$G-@=t9*coXxqYo&3JNvbVL(sbm&$<7xrb|_9>ZDq?rJU)ckv;kGEtzE3FXxR( z^>iBs`a7JEBP8 z(YtuklWs=m1a4O00gelw#=Kj`Q9ak!H;hej?3pufDd_0kUoU}H%P$AKJ7zLSS6Z50 z{jpq7VW#kWbSAx$T*c;#)2*)DO2fQ5s=${s&U>gEoAaabY+x)pw?f*~C7H9|^Hbw% zUy0WC8{ADv%-JOE&1TvIhqp&*YFM_F6q(Z3Bu(T4?Dhxp^ad(`>>nv5iUBTw9)&+J zkReLe)`yKHpF$XiJ4$f^+{aqe9D077D$O2=jVB<{?H;6H({p^%Gio<+v+@_@Qv^w4 zoZYDEklNHYQmszfy&?RK^`O%ScdB6{1GBorFA|jCRhxihOZUJvs$Z9i{1i&#xZ$FAQ4YGjEYYYq5B~ZB2<<5x5c&PNYc@9r0(Y> zzQMD{cAOPIbPVM^epf6a^xyOGICVo9J__z;45@7u`<)TI0DJUoicW4uz#?6-W-^A@8-m3fK`S< z$fS-ofY}yeTy)jwf%~SObT5ceq&?+E=afbAbg~NfOGp%uVl~-QuvRk2M7bVXDNh#7 zB|G7-wGR4v_lWiA{`3S@W#ywNhUXL1Uaz1xc%R5mgjU|3epYbOiHZv4R*TF_6Zi`o z&zL}hSmjfQdMM8*5dj?hes?7CJkXAzvf!2)DZn>H-JAC7UcKm_I=>D1=$_QOb(g$k z&2T8{uY9I!J;V;gISILS8M9jJQYBl;h| zf%6&xDOf|ms9-O)&<9Nsmr9Q(j;ie6QcuAzl(0&KR1rW9WZ8TU;n8%Sqv&Ic!rsx;^0^YPs0%6tYIX^>|^&InbJvAj&fAvi6+6h9>bt1>7A6 z7*gvy79T(2m|y7n*E+x)VvQ;dEV+%o4bdoxDg#i>hLJVsbra4uJ!)5yc4!VZ9RFZrxkMFQ$q zm+E^R4kBtqThFOBRH@8lyZGe>KzqT$F?awr{y&`AKTOT|Y56L^#6=2NObvwNsLK%( zX~n*=sx0h8OdA1(Dp5OC9pac#5TW?RL6df__=F>vsN7KoUs6u|fiKXS9*6PColM1Q zfxEV8y?Dkst6wpqx4OYHame|Ipk!eNZB(yQu(YFA=B}@U_{AkR9pa$<$&Et$GmC5{ zd!t)rK>i~{=%@i6Himhxl@{KLHq6vAi-^NHx`*ij6NevOFTZHq$xEvKE!#mxlCpIgkyO(Z@YX9U|l@^>G34d7|+ZLV` zpx72xtFv(`9f19wbLWc9C#q{xt|}i)*?v`(x2>N!6FH}!$dpvoO>pScfP!`VrgJ_T zbUD;CJg!?8+A<|>v{9Nu;h8?M?6eW)3dkOrc&d)$QlNxj zBjH&+Ao@|OzzO}T4!~)qR6a*|S8^B`nO98h?IwgC!%aU;5GLA6VyLUpsCKSkfD~1$ zplxdQs1)DC6r@${myyW{*Vc0qRwJF=w@lA;z?NP5_A)K=MvM?YeqRXzJ z{^rCW)kK*ms%}a%ayNA-lwO@D(#7aPkgeoWWqU1H7 zvj0iAI*D-&X#I4l5iLirzdv~VTm+z0g+HtyIBZxir=HM^;P*%3EP#IXjv^|d*`n?t z0MH*?re460IHKr66Tb)cn2am8YrnN>zdJvBj(2lI7|M>x*d^jAYFIAKKZ~46!|94a zum+LU_<5Uo3Z~qBFXYS0jJJrA!Pd7#utHljef8Y+>%>)>`DD>LF40X>2JX;B$n zjpS}tdrHdyQE_4PO3md`Q~d_z6=B^1H>;S#V9yVATe*F?nF1Owr&Z=JLPs~Thp#jy zY6(mCabJs!vi9KmK@-#ChtI2kfEZW5lcxlyP>y;l{yYw;- zhZuuB7I11qhmyK=W`@kBcyGQwK5OeDxhsHJ>cX@40p_aU_rlf)AQqmD);477ef>+HslPu}1qAhQlDLxB7DI>~F{zF5 zic$h^OA2A`J?-ZG&HEbvxFFD98>QAkPBe+%3RSnoSKB2Zr0mZ%!=*pvrfe`b1~Rpq ziutmDqu%)^cV`=%(i~6+@p{I&`yh%z8Vw5|O53vS)|86-Le9=$_2EIR8~Sk91@J}J z!D_v!uLG)-%Vm1~YinA{ag*|p3jHGDNyQE`COgpysW#CdC&A= z*RA^X^zmSfVf**<3|&(-cd*4twdC1cAzEd%>41{U8h5uK=bDQ*N6$23#=UBHouUnG zwlx#R2<^cu>)jxfbBDg3dfIg#+A&PZ-d%gZ^e4)Wlm`0ORq6Ud=7Q6ny5SN2Kv&ty zo!>ku0b>F~0fM<4CYC*82Fo5ff)~S&j%KBYS%s$Kknk6b+2Z@yv+XlD#81CgjXciH z#yGn5D?j!SpmYzjxhFsWF<7x+cQPMIg7$|Ff}(xM{jQmXG5FN}YT&A@cmw4NrVG-x z{+Fd4(%rkJ@{l0h^qJ|*nnob%C7;XOaah3-J>|`M3;74R1E2lddhu($Y<~}~v3g0k zp-hN!=RjI6-kuum7B=A z7j6Zq1vgFdjZ#MwJiQp8M(Nqm{3G5 zCFnyOmx1lR{7J(+F*KUtNCKU0rQs=t&0syT4d*|(k+%bQMPj3pM|Lf+SjF~(Bi#e3 z?KavlUSU5~Z=_!kh<)oVh{turg|m5o%4b6pHmso@R5q(xiG?X{=sCi4yxc!7iPTKm z`tr$ST6`{=Vzny=JF(|n%M1#N0;Svt;3rD~tZ*wb_&KpP+l#dDY1#;YmX2?>V~P|=dYEF)l|8}aQB)6dc+AG zlejPs%OL{ASvCH;E^*#j>3ngiyo<%wd}IEV;O?A0{Qb!Ch94%xpwLoW)z_{6 zGdJjRb`sYPdduCoOV*7(PnLq}4&g$cMc2(x?IE??B1FHhr(SsY-IeNSRI_6 zsdaV_^)|mTFdAYkO1;An@t~Su2ti}Lo8iN6j5vcybuo39le>U@%`k8|Zy|3f1bE8% zs8x8OgMuUOOFNhgizJhLkF=oKeIgQaBje1<7iLz~SX*@tx{%>K&{_cI zT2*>L@hTGAIq?HKsnyKRgxio-`>pf6d&(ZjK+?W@$E)SpCot;48@t?~VPsVU9Jcv_ zjq54-6|=MmuZ09+`!6fn7~ z8~lmmBBiy<7%idzJRl%(kiY0^XEHJGEp?uVK`RMKzLIl>OA1G@{v~I@yU>GU1B|^x z!2s_;jLUgixg~Dz-C!zRt4T_2O71Otvi(@JD}w#_2gKys;PE^2RO|RmedkpL)!F~? zY2BE8PH@3pf1@8j@n+1g)((CmzSY^ngZHdRPmRE+#!KN=sS+)Z3NHGGgP@#ONdE@Vic%{#2wR`_n zdQwDzTN49!_KMi;E1Y?rnX}HL5%BpvN^Ug<06;&83T&M$!$=-Uh9tj~QbF@mYZhCB zvvAoNoS0UJW`Qh}CL&M}Y2b1o^W>;(Q}uh3=>h!9pzPz6Z}P374YxjumxUJjE#FD0 zp_OCMziLnC8VK^cm@De#>QAigik!yDaC5w@rn&9oE?=pY=2Pg5QMa?$EBZJ1cBR{A z1j8wV!bue0LRdPDFY4gS~L0F#i0U+wkk@7VoaIgCQR))AN zjGAz)3tfwyBr&33)r1S5bVCJ+ZY=D1ow`qTH9<`p8_-lj4rbdb%vvAH}AH)4w=%mt~;yBP9EPH<`k|D zPkO!>M+=>X?pf7JGIpEz=sllm2*(T9^snMdDm2@w6v-_mitq}}NRMFVbq0Qq_qVT@ zqpm3|mhjye?mc_n+?q4hO|xp0qG%;i5;!W|6VO_WxF{|=J%Wgtb}fvq!;&>rrg2E> z`_)ePXB!?Be$aQ$Chqn5h%XFo$z3mAuOuIsBZ>-z#Su+SKxNfa1_{x;%YRFqgIcpe zU$K~duCp7D&EKDIh-P{R@d7)d&=fW)>$0CSo9jFK`p&vv-t(EQhl7K-t&fiyiOFOJ z+G+Ek=t*APVgu`FO>mRIhy-hFE~jJUBg8!b0xiuPss^33K7kxBNtkGO>4}MJ@#@I5 z?f{YFiT}>a_wOWIdpagEQGpzV*{g%l&Km-_$_}9JVRfDLJDkHQTnKA@7MlxJVF3Ja8z+nw$#o_2Nr68?t za_KO4$wi5kIs1WH{T(NxKcvzcMP_IbB3<5x{bu;<*Me9*3S8AM6g&yN(Gz&61}qLRbH-G^%M?1_@YiWD8FP*7m@meFD&i-ai zmes@gO|8_{!}1Q3$tx$4OLKAxN2t%?;48U{Pki1xM9%5ZEdFml>%WOb57T|BhOko% ziVfb0Ngk31+W~aF!qN#FT%aCRy^_EkE}QuMq0q2cq7!;e2=V3aMU2;co<+v)=300; zE^N0adJEOGnW)6h6{xt6nv@BkOaU~ZnP{&wwhp5IB>)zl!m*103rQzwGg)R=dzua&-bM_YxTfR&#T zR%bdQSLcv*!tKcI8%;ce<002Z7^5Hbr3|z>`dff3W0$@~bArYOjXMruPbCyKqav&` ze8h`i)o|Ko4LT(Yv^w7s1E_*GhVcy@OW~0;vFC3B$(Mk<2SLu4mQ?8xn4*}=bn%x! zv&BG_2yrzHEFni;CfA*6u`SjB9Fa(13trraMxepe|KAmIFWj^aQ0*pS9N@2285!e9 z(ptA9fedyOwVaFw$x#Xx+^o1>9CKQu=lL?b*z+d4cR3+1{}P|tP2a+abTW@(IMxJ1 zy{|4Rakt;dfpBneArpqvP?$2LJEvIU zOPp*B-}i;rmc3Fv9DbH$GD;)|TArc0*H|(FUepz!nakB-{_f*gS z4EN?}{+`rZ8QZLem#y2vu$0$x3uBZ>keE2X1R?}qZuH};*Ub$OAy3$wc$$a#`H9>R z@{0E5uoas7_vC@mqctIe#+Y_3#Hs})xXhh~k=1TpXS{Njp;Mtso&`C*3gau#{|{XQ zdqTCebfM`TTB`D64i~0$pIeYJd{%(Yf)&`oHI^{ z3@_;0!OqmE^RgRDG~$=x6P$aF`BaB0(*0SC*MR3nXC5F)Eld)@ti9OVnCnbO{aB0p z)-e?vlRzAvb$~LbTA+M&`qDvt*m?(HXUQ9SjahCv4*ia|;D;+xFh=;n-oVsDVL=>$ zyKdEkdKKSoCH3V*gxWQ3<)v6|8asbT^1b?t+~tSCOFAZo1f^VYpGpDk+o)bR-uGx1 zT_ybTCqVXB)KBe|@@Tc&Xy{10T>B2_UHYN|@$JrlME^OW|H=Hf zFQ0m$YdntPE?v@GpXJY8O50P7QHkhwVAOL2pcH)6KZm!tX%6AJ1FW4Xi*>?|K@Zk? zay}Mgffwqvs0v2-!#(m(yXgzgWN}TpCcW-zau%DuA*gzY=(;=z;LTaP8GHFUWw)YC zj!R}RX6frTH-o3r5e*dxdfSQ0u2?f9zrx2Y}&VX881jkgjYsg~tpT<}IZofJ4dQkLln2}o_y|0hU zGWpC%hA$j4WM(OCJ_=*FUi)m2f6Z?$`|?aXDpS}^bD3#g1Ana+QNChpNR;TUcu8AJ zjn_0~Pyb!ifw?k@IZ81kZw@^o9R?WO-Jipn*8uO?v3|ps6;a5hHw$88C1dk?Ym>BzIvfUddkX?8lLyZ4G0j z)=)XMd@^eg>)GYQQ=3a?Rsol}vha4W5cGl#EF+!8eSfETBf!TH&k*|6xIWu+$+uR& zl~;%(UF{<~ZUs*{+Ef@mF@grrNm`%Q%1yu|IjRPb#bYgJH$*x|;X^`%qG6-qFD5Yp9RWezop0Lldrv?igzwtXHnVRlCxT7o>*FSYlADQ6XVl4$X7tal z&t^7#d>ZUF(-k;8WsZFx)Oe4om71~-SDr_sCjvnb;DobW&OOl0ujFBgVsIMt@w@`o zRvh?ug!)ZeuJ{Y=Lxk@5^s{_IsWU$G$*qXk84BxM@)*OWO+DR3u@*>HMT%GIWT1gv z_H)p4^t#?PSuD4YR$*<+PM}HoZ zrVJBaL!qUjI#lAQ`gH>a((|9`6NeXQy)xz2B}htsW%T9A)k9Vnk*uV4)=3hXvSdc# zRy#Uga;s%R@J))h8;6_ImJev)V%Og1EM$hfs%)h`xp6DMy+FzUS{K}oT2wrk>UcrF zuXBHcDmKNNO2#<`^eFYmBCY}6ee!s|zI<0^54gK)87ln(r;i;6RuMK{2u5%(zFb~X z5006MPLXCK+;n<$k(K?iG#1WBHx0tfCQtqYLDo}~VqEX5S#DP(3wOI)oDcQ=44{E0 z+*!8-ytLoP4x#QEkoVJqAABNa=NVuNev)nk#h+1lfNY5#F-#5!yD=~>>2!FZ9_Qx# z5;E^Y;ceMLyi0 zVl=J6SZuw)*)zfgIu1L28gXn}HCr;xc$mO{78D@fv)pk)5H$x2oO`5^glnSl(JVHz z<$Je$X#}8f2BlL%XW;1^dOp;4LQ#d)P7t+FmGA`ufBJix1jM4Bi_G%^z&6N=z(EQTytt7y~pU3wxmqBvsH)G$a0FfzOPRabemrg#PinJ+RsEf*p?e9YHkLgj1}Yi*g6ji)-*~x}Lro z9r>$1=}Ws8{s~)fIP6hv*VxcG6MaY!`dKepuyYdtC#2LN?!)K`;+GP{+-?Jo}c({_s&y&nqD{ zw9^80L1XP~p<@y&`{GeCSGeCmKBwg7!+SR(%>Wn+=f{_6x=kqrK*PO3_BAS!7sJ2o zSHnU)!G7+eLec~%qDB2gHQgT`jy5<;DrLoFfUARJj$Mj69E!>Z@p3BzD7cj7nO zO(Fe;n#zZlTf=YZ6j?{5sbI$X5<&MCvoaJe#o}!`;=1=ivo}(bJ4S=G6XSp|g5nYW($J9@wy8)#5`=-Mq`Ucma z7+-lV`YwQLK=R}5riVffw^O*PtQfa3i6gW7=g{z{%K2&$nj_C} zrX&gk!z8@as$!S0$eK6%>m*qaI1Q0Ml}YbH8JqbslGqr8 z@-W&Bh5#G?rfRMut9~2#X?cJ7*_$66OQx(m>b+<@HX$uJ#Wqt9c|IFR3YyaIC zwgNBCH^9=vNI+@bi}ai}H?lRwt;oDm_-zQQCUu6_Cn0Vc*UQ5lrJX^B8Y-%tyA|>b zV%q^I2bd}t`7gQhJHl2^<8~*eOJW7(;>i6#pRi1quJ|lIi}MLgGC>B}U#?;rgrGoG5-zZ-1Q}M-e+Sh2fF~>EL=r^&~_fkmp2U$9<6~hq7z| z+#G>8T0@+BT0-l$V`+OsQSA2edTb~6DY(2^uKDeBR2>Vl zvCo!PXpFYag)Ga*Y49w?YEK#u8`dq`3#duhuG&b?^_-CLuV>9AYHbWZ^|z05WatOw zvd5DlmAPDLN%bcvTV4n@2*&058P{8KB@|Xa#;0h_eG#bT;_ce%sT%WjThl1%I(Jwo@V8q6^ORdIRSNq}nJpHPv2Z4qn)$*yPS7R%9*oH^BtShK4lyan2L zq;)2{oZT*modG!-Na=vhBD`O9p*MxFM@}+vB5t4F+0Gc`XETWhU0izCx_d+)2bh}z z=e9?O&(CgBsB(>J=9^@_QqHzy>5q~SvT5T9Bg`E{^LI1>O=uq_xV5|@2E&QZskdpe zi@(2oJ*-oHnNT@mwti47MGPQ#kl3YqhckwM*Z^4F;ubewfdDA50XPZ@AA$O!itLU=~mgpG{5o430Ajg%#D73LFcDS^ry^v(U- zMoxYw)RY*3m&mj)nTm$@wp{NMjVbc3Ize6Abf=JAgw3iX6+%r!jzo$;|I&W%n`~$7 zy-Gm%+n|)p)Js*O1)dNn4Eq>mpt(rE*n85Kc@RJDD^J}{l2;`d|PwxC{GXe zjQJPx5L7$T7hg}|Z__{3w2QLNzbq4i*J|zBR(~A`1SiT^qTd=lQZeivaWr2>C}G=X z;)`q$g(87k3cSFtXDT;Giq@$FVLSHp%#fyc{i%3YfDv)rmb^oaKN^onm@=-Gdq(e zc>ZpxR$P>gxrJooNIP6xZpP}Ue0qRF4A@r)=8~D+_Py;$Xj_k^wl{$lCDhhh!EKWpE^3Kc6R!n zuScKS()&LvXYmYTv)?|SV=pS8SlgQ|_h7RIhfCe?Yio~q5@SjVwO>d{%@E4)&7WWh zr4W#OlH{Y#qV7J)I1)2b%YZ!Ys&Pn`wWwCe`2rD>uv>t|b_*>i5m0W0z*>n_h;R$; zU2KL?NgTx1Dm^723VW$YbxRZSe)}4+%xBPO zmvKnOWuzFZ3kP{Ad0{VIQs?d@OOd6q`nn-W4S6ot}o6-Xk3_p+>yYUlb) zyVJq4g=JM)oP!8LYmaJ;v)R$fI-h@PO$%7`DC`$D7ne zkq#1B#d*Kq7+6aL@O*pVEh>V`aAVN*UU@ErmfiJo9)FbA7ySQ*kFsc24;IEZ%B8y& zZHZ>CP99#hx&DZd{mUg0Ibn&Q729Lk40p_@BeA{7oxXENxO4r3`LjM4&O%0);?y(nUF4Mk0g-g3nB{Bb0c1l=zwI@Rsu?}(cz=TvQVB~axIC5#*vJpNE!o&OXM zL(Y$!Sc-AE9Et20M_g2~iO9E~x;sQRw>>@Z#vWCg$GSI=6q=}T*uM-t1Ex9mcAV*) zBx|5h^N5Cdn&?qU4llU#j=wyPdI4blc&-{DL$ABrfACQBqfm`IAkR0fUn8R)*M#Vc zAa)i=vwA}j9p7M9|J(1^7g(}}&yOUc=t6_A8}g8tEw^*0wQ{#JCu^Q-V?!9+iqXI+ z;xTeSHpMTSlwQU0f=;jsfz|L~i)k97%xypD!w34lPs2SIy9K$w$Ur8dP@)<<9rPf&F1Y8C8k&W=4BbT?avNZuC;w;X*_07U3Co#SA~QkVS9LA8GrqfOR7& z7=CuMgd1X87pa?%6c&0hW5-c{la#V%-`YVlct4&A)-u31?Y)#^K{ljBR>n&T2|O*S_*wVVYxh?#>pWuu|E`wuO@r*nB43rt&T~&TOTh3M zZ)=8&2<1jBu-CehRhx48Qoq9<{6p|11b3oiqeyOW*}F69`v8V892r~C zUJP0)tJw9OSfHIcsq{*;43u!V(g6>JiE=+`K-nXSbVo<=vzLeU=57<);#u$p6+~EW zTg<{Z51L%Av^(Oh;d}uSQHe*x!<)nc0+ELr!K+Mn%n8_Llk-8Q^B+$4SE0AL3&@Up zr1fihdpKIjabrXOenrCb`Vb+tjW62mT?)UP6UkJ6{Z(?ge=^3svDuli>~~*t-G8*F zDmRRArSSAW_+zES))0t-`HPn{mk%eiT`I!j8@l?=rY~1(6DA{ld$DDPPG=3Dj_%DH zMh^}@-0Yw4(z5Sn6c17(eM!~m=xP(#%q!LkKyrrdG&ey>~hI~CVv%NT< zo^=kz*6=_7S?PGq^x@X5`1SU3V2oq>_HYkgk~O!n#z?g0SYN_eWVdR7P{4{Ww{5Jw5_z$@t7!jy9cKOehWckWx$Ovv6`c|!Ii$PO3#`PEfx zdxK_yQy)9w5r08dTT7kYKFR^30fPYoJMJZxJfa239NPmILJy5*q=%XZr(%%s6^z*s z_}Vh>(%QyOJXH_>o|%cYcj{OC-9?DlIl$_g_=uptWX|GXHkt_T0~G{Cd!O}LH4CZp zruA9RR#Ev1$Q?ivq-FKqAu^=9d->B%9B;#SsxxC67O#hLCUeVv30LTZC+jtcIW`b@ z+NJgE+hW=F7Fc!Zf@njD2=&IEs9dxqHv~E-P$8BldLG}uB~FiYjg`6vU*y$RK-*^0 zZKYD7`GRS!aMnC4fpyV-DyNf4Gnw5M&vj|BS6AOUq+7OEH=rLXBT3R-=IBznT_fib zy3uS&{JA`QZb2cTm_kCpha@fo*LCrYifM9SB-5S{Jkwm&O%#L9dTbloXJR9N3-FTA zLOGZCl4r4k^$SP33sTc*v~H}zezL|uKQ9pT%9|gH^N<~L{q~sKf;x0SO)a=&R=Erd zOI*)$nEr6FXG{{VnW**ojnTN^Of=biTMA}k-=UTf00aeEp#i{On&e;hAX7mhVWW9D z5$qyJVGQuPwYY?F*E|V4hiFn={4+XovG=@>?6V5F?I0sj`(1}YIa)F3>N+CIZ{GuT9 zCt-s`GQ)PvO>DzHtzwzx$sP2|+-BcMh>-~O7G2~ol>|c&YKxr=AASRbS@a(lQ}W#E@k<#tB-s$duZmRc>Jy>!D5-9VmD+jfu2M*YLk1SJ}#5rQX zB}+3s2en_RoSAq- zOe^bZD^EZcGMxKb3cy?|O7}^h1*1A9zahufnpqg|8dIx3H9xmbS^a4VTXycb)jWH7 zMm)Krmg`mZEUSP+HlA>?JS0A17Z!e(J<>`X4_wI`&2~js;e6|476jwxFxz+@VF)Vz z7S8|FnHY^p&*U9+Ist>~&M?@HXNX!DcMe>>NH~w^e*?Z;`T+4tm_CCg4c6-XSuQ&o z37G5^Edm$}K}1p>Vt?mRi!_Ydqi?E#^MR=XkH&$ggc+qpBwo>RPYMNOQekxNtuey+G823F zB$leBb$|*4(##o`Ei{z0P-+}QPJ6xn99p0pFDedHW2n@ms--&Y^)t$4+M}b3e3zz_ zDMxRUbJn)`E09?BAKTRqe8+SYSKFnJ6qEt(=Mmq}S#-8C9-s9TKaInn6@#Ey$vML# zhsIg|lrrO4?8dYJ!rdmNf%722XFsag61VYcFqNp$AfqxR@sd5>{9U*ujCqF&Xn3i6 z|CxTQc66q`{iKBM===L&&4_JIV8Km)tq(xqX56>h3VuAM)xpez=e$r`8ONa3Q}+4~ ztT3r2W-9;(Z+_+z`&Zl3ZEf4o9mF>C+qqDYP=D+OljCMi?dq1R4OSkjeOVolLL_v) zO!+|bn%k*JrT0;_OaDb$Qe>WM13gFfvhdXtoOzC^qvpNd|H(aKb_E8&Pd}gnOuZD{ zXbwq+B(IoaUh_k121|pJP}w=OkVc0_fef@70#Goq|3V<+*obUn#Y>av9{kgw^zTXU zgj+o;Ey`Kt#6!%5w|Nks2_NNUxD8uEj^|27FF@KAn@ zlUm17W@yHVt8n&%{JOHLGt+*Y#>-v{X0^I`XS5yMrYFbkN#IHCBxVe0iFWFKNRG1s zV2aG4@>M->&)VK5y7&yt>JY3;EwkN3VZ0!fgiEh zTk$nAE;{N3l9zc$ISQO=k3WUJ62b$0@sd{UOz>SLHT6}?fI)_6uX{jZ?papGHpbibnF_@EBSB^dyU#hp)N8vA^YueQ1Wz%yN3T(d z0GC5>_qTx5|JOI0Ug+qhGW=kw9f_LljfKyursvPJ{kg;L2#Xp9;Ft&OPzStwmP{-| z%s&^o$S23}s&eEIy_#PLG=C`h)Q~h`0=gl%t6i^vw7}uP`DL%&=qhoI=!oq1K4Kt4 zmUG`rV7BzxXODm3@627T!wNozlY7H~9Hlg|Wv93^cz&HE4RSA}K&mZcM;xno>R4vY zpMSIfiG3E**+p1I*cIc_^l8DIe9_@GgPQ*PcQflu%L8lUX+BLB3jcx_DIfKRL8>%7 z@$Wkay96zk?N6+knosbZH#Pg7oqvRjVf9_RO;!Y-UA_Kt^bFMcEaS~Vm?rf(QyIUe z%>!#X0epHS8>jexT1_M~fqxoMF3AFOv73ZHgAaRaN8cxIqg<$lEy^M~P_E`^i+ob& z!XLUdJ`5M|@m#89wdj@A`sz}f!?kr!Mg-QjMXvwy3=&Ax*>oWP6QKxR_nZED3IVL( zjSpH!d-aM< zpugxy3!&|V(dW*Kek}w5N~3Ue1dZZ6)7&km;rJS43Y#UXUS3JMo#-1-kew{T! zaG-V@U15~owl^0Qe01UwVvMlDb#ht&Ty`-jBros&5;*b>GmPodWxOadpa&tmm;l;p z*TV>(OS!syAVlSK;`r=OoR^YYf#vo~zoQWvJ{J?#e(86ts!cAk+LS=x!a1lPkDG%^ zbF~};$fFkN^t}aj!019Pg@)vREg;8Xs8S*!MFu@i<%&QQ;K#lO)CnIx02BsM6_lHD z775|;?RP8BsMt3Gxs8EBo1%8Uk zpZrDg{^UFfP@Bn;@bKw&2Uf8ZwQk9s6DAca%K$6?|Lo}S?favb?DXW#(TC~LOYbvd zZ;lT?ygNGm`Q1y{k=ILfWsEXC{ftEd3N7b?vH_@RC63pLjUa8E>WvS^gY$pJkW@hGaN2;0cY$! zX`+)Z3mmGGuEYr>$3T7ZvCC)FxU~R{UG0=XlH2iII47(5$d__ajBZf@N0d^{-QcB$ z=xs_-TIP1luT8C@hZs?eugq>I=(TbQ`DCY;2euGK|(+7=+ZF8&zwYA!W0NusrV&7GssMPv-d+ zBt8!)3km?+V|B~U74E4Aj0<&}IrY4U__;^p0HyOA5ha|blCxu z7b3XsWmxX5V^MQ>CbSyf3Fj~rjNhC@Zz-u<_URwXeea=g79n9s%t_^pvKsd{u(nN9 zFeE*xW?PJXQ(LE&M=OWQ4pGA%HAE*>Ay&f#XVkM%vJY=hQPKq^=8!ArcY1p?Xmg0^ zs86(Q-v3lrJ~}o<&pjvY!^ZV=r}BDG8GcVFtwnT?Fc#e=&)8XwF#tCn zTZmHQmen?4PO(yOQ!!s-X%RO)f&~!J5YSvfpxnHI?tObktq|^cbnV}!KlHajzllNP z+lBzs!@(xSAnPC8;;7qEU@vCDrLoKKk+pNgPNR4)-D{?j7vVa(?}m?t9#*dL&?K`D z9)|kMUcX}9?{xo%Bo=}mFjLw##vG*IIc5bmy|X9)Df$$e=ysCB>2db^(b@Fm=MPDs zWUP`U8@+C`%V$O`lKNKqLvxk$GKncKXt+ejl%_O2(DA?wBMThAGqvz7uV-LTuD# zg&rm-AMz#0i#_}zk&!p?={^W!Pxl0Mbf=*h4!!dvZJN{j;}AvH!}!6U4s`Rq=^q5Z zO3&1X(;L&jd&T_S>blbC5iBMS%j?uYX!D>J)3%H2l-+CAMdO=)Bf~}I?durYpPgZs z>k`^$wOEs1^6A<2X;fMN2Kc#(5^Rkac$|Bu{Z4yAKcnHs32~wTCWQq!xJ-DQty9Zx z+b|Hk`ztmM3OIl&AUX(K{%wc>5t``8_z76?-qrPVr3t;x3+8R10Eru{A1+(wD zzG>?g0W|pRg(05uyGL_~mo7>yeUuZb2Nt4QZ8+=)RIu~Xi7~iKmljw^7Yf{f;8+uY zy^s>Lk_R9PB#5I8Iy`4BQw0Fm@HhyY6vH`SAZxH?u{PZW>CxaXp}mF1_fh6Z$sAA^ z&pa%MPL^hNoDPgRkCzlJ`1TgoRP<)^@U`qwp$YKnW`e5^V8Rvdw_|8vB%uoc>SucW z{e?2`2c~+o=*mu;ZEG+R)afkX(@TzyYBd=ujyFy?i8U-wI?If`-poZ3-PL4ld(5c* zqqFLQv+5t6z0~@tn1+yxdOdnP6?(~O%3js!oH_lwV>z0C*f|9$x?V`}4W`-reVO_v_HQ#VIMJ{6DMMIsE-8l?Hrsz6&o-AB#7mKZtMQ z_dsXp{yEbYra*F;P5>K8e%#W6_&` z+MLI5IkpnVxjx|^#@_tra=rNYw=@5)Tvv=2kEHeF*70?C^V3w?L(W?1aefv3M%(E5FRo2rpts1=au;rpdmj_z^(gCio`u+_DhA5ClX5n8ldr%5xL~(R}JQzQrF^pxsPw}qe`^ymb6K`v-d|!-! zwbD%argVQ}Yxzt^(T(UA_ytje9^|Q*hK_g`-LS83Z^p6M8hTpXSBm=FUm9yptkIly zqIy#Lps9E^q9--X)8`UBIu&tmU(*M11mbt)?n*60?rAZ~m2`XR?WgE}1-+22maj(V zh~q8nfTP48QvFIx_`Bikxq)~Ve?3)mT7&NL?!UaoFXx>n>3R8zk9C$I>Te<+J>L@N zh@TL5z?b5#;?w9L@fG}4vwS3zvEEEMHI!{I z9+$|)d)Sm#Hl`OTd_Q=vSv{p8RZrIMJV4xyb~o8J94 zLEmyqIzjFfx`#M=@}l@#>fTBl)Z_2OT)qby;bM4;`8*8aqogW{`6<4Egg<{59-N<_f46}>uZX$ z?;=)DGM?0sS{a)p_c%WvJ^9<^-iE%Ucg60gT*ev5falOY}2pw||-^_mrHq z(!X%$c^#~qSIgTsorUqxi?lJWgLTpzoFihdHn(&&8y^x>n%k9gnb z18HT{z&Yt4yV< z<;J2X?`@~Wyx&dPnN}Bf=G{|@KIuV<`-*ywpbv^q#6aHLF3wrD7k`SU;Jp<0l%1=@ zo2ZNaxC32`zl69uy(=yGGwGe^%@0Lm-?l@%>uK`4B6@fqje5PYye0n}>{o7+GnQ-P ze2kWJPpLWDW4&1Y`EPz5y+%A4v#>(EgbqL__&%W=FF%+XOL|%AV4 + end +``` + +Or + +```elixir + Ash.read(:resource, bar: 10) # <- Adding `bar` here would cause +``` + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8c13744 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,4 @@ +### Contributor checklist + +- [ ] Bug fixes include regression tests +- [ ] Features include unit/acceptance tests diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6977f1c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: mix + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml new file mode 100644 index 0000000..e015e71 --- /dev/null +++ b/.github/workflows/elixir.yml @@ -0,0 +1,15 @@ +name: CI +on: + push: + tags: + - "v*" + branches: [main] + pull_request: + branches: [main] +jobs: + ash-ci: + uses: ash-project/ash/.github/workflows/ash-ci.yml@main + with: + sqlite: true + secrets: + hex_api_key: ${{ secrets.HEX_API_KEY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e79954f --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +ash_sqlite-*.tar + +test_migration_path +test_snapshots_path + +test/test.db +test/test.db-shm +test/test.db-wal diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..44acbf0 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +erlang 26.0.2 +elixir 1.15.4 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..711d535 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cSpell.words": [ + "mapset", + "instr" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a931968 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](Https://conventionalcommits.org) for commit guidelines. + + + +## [v0.1.2](https://github.com/ash-project/ash_sqlite/compare/v0.1.2-rc.1...v0.1.2) (2024-05-11) + + + + +## [v0.1.2-rc.1](https://github.com/ash-project/ash_sqlite/compare/v0.1.2-rc.0...v0.1.2-rc.1) (2024-05-06) + + + + +### Bug Fixes: + +* properly scope deletes to the records in question + +* update ash_sqlite to get `ilike` behavior fix + +### Improvements: + +* support `contains` function + +## [v0.1.2-rc.0](https://github.com/ash-project/ash_sqlite/compare/v0.1.1...v0.1.2-rc.0) (2024-04-15) + + + + +### Bug Fixes: + +* reenable mix tasks that we need to call + +### Improvements: + +* support `mix ash.rollback` + +* support Ash 3.0, leverage `ash_sql` package + +* fix datetime migration type discovery + +## [v0.1.1](https://github.com/ash-project/ash_sqlite/compare/v0.1.0...v0.1.1) (2023-10-12) + + + + +### Improvements: + +* add `SqliteMigrationDefault` + +* support query aggregates + +## [v0.1.0](https://github.com/ash-project/ash_sqlite/compare/v0.1.0...v0.1.0) (2023-10-12) + + +### Improvements: + +* Port and adjust `AshPostgres` to `AshSqlite` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4eb51a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Zachary Scott Daniel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c632cb2 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +![Logo](https://github.com/ash-project/ash/blob/main/logos/cropped-for-header-black-text.png?raw=true#gh-light-mode-only) +![Logo](https://github.com/ash-project/ash/blob/main/logos/cropped-for-header-white-text.png?raw=true#gh-dark-mojde-only) + +[![CI](https://github.com/ash-project/ash_sqlite/actions/workflows/elixir.yml/badge.svg)](https://github.com/ash-project/ash_sqlite/actions/workflows/elixir.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Hex version badge](https://img.shields.io/hexpm/v/ash_sqlite.svg)](https://hex.pm/packages/ash_sqlite) +[![Hexdocs badge](https://img.shields.io/badge/docs-hexdocs-purple)](https://hexdocs.pm/ash_sqlite) + +# AshSqlite + +Welcome! `AshSqlite` is the SQLite data layer for [Ash Framework](https://hexdocs.pm/ash). + +## Tutorials + +- [Get Started](documentation/tutorials/getting-started-with-ash-sqlite.md) + +## Topics + +- [What is AshSqlite?](documentation/topics/about-ash-sqlite/what-is-ash-sqlite.md) + +### Resources + +- [References](documentation/topics/resources/references.md) +- [Polymorphic Resources](documentation/topics/resources/polymorphic-resources.md) + +### Development + +- [Migrations and tasks](documentation/topics/development/migrations-and-tasks.md) +- [Testing](documentation/topics/development/testing.md) + +### Advanced + +- [Expressions](documentation/topics/advanced/expressions.md) +- [Manual Relationships](documentation/topics/advanced/manual-relationships.md) + +## Reference + +- [AshSqlite.DataLayer DSL](documentation/dsls/DSL:-AshSqlite.DataLayer.md) diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..f81cdd0 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,54 @@ +import Config + +if Mix.env() == :dev do + config :git_ops, + mix_project: AshSqlite.MixProject, + changelog_file: "CHANGELOG.md", + repository_url: "https://github.com/ash-project/ash_sqlite", + # Instructs the tool to manage your mix version in your `mix.exs` file + # See below for more information + manage_mix_version?: true, + # Instructs the tool to manage the version in your README.md + # Pass in `true` to use `"README.md"` or a string to customize + manage_readme_version: [ + "README.md", + "documentation/tutorials/getting-started-with-ash-sqlite.md" + ], + version_tag_prefix: "v" +end + +if Mix.env() == :test do + config :ash, :validate_domain_resource_inclusion?, false + config :ash, :validate_domain_config_inclusion?, false + + config :ash_sqlite, AshSqlite.TestRepo, + username: "root", + database: "ash_mysql_test", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox + + # sobelow_skip ["Config.Secrets"] + config :ash_sqlite, AshSqlite.TestRepo, password: "root" + + config :ash_sqlite, AshSqlite.TestRepo, migration_primary_key: [name: :id, type: :binary_id] + + config :ash_sqlite, AshSqlite.TestNoSandboxRepo, + username: "root", + database: "ash_mysql_test", + hostname: "localhost" + + # sobelow_skip ["Config.Secrets"] + config :ash_sqlite, AshSqlite.TestNoSandboxRepo, password: "root" + + config :ash_sqlite, AshSqlite.TestNoSandboxRepo, + migration_primary_key: [name: :id, type: :binary_id] + + # ecto_repos: [AshSqlite.TestRepo, AshSqlite.TestNoSandboxRepo], + config :ash_sqlite, + ecto_repos: [AshSqlite.TestRepo], + ash_domains: [ + AshSqlite.Test.Domain + ] + + config :logger, level: :debug +end diff --git a/documentation/dsls/DSL:-AshSqlite.DataLayer.md b/documentation/dsls/DSL:-AshSqlite.DataLayer.md new file mode 100644 index 0000000..0d68638 --- /dev/null +++ b/documentation/dsls/DSL:-AshSqlite.DataLayer.md @@ -0,0 +1,280 @@ + +# DSL: AshSqlite.DataLayer + +A sqlite data layer that leverages Ecto's sqlite capabilities. + + +## sqlite +Sqlite data layer configuration + + +### Nested DSLs + * [custom_indexes](#sqlite-custom_indexes) + * index + * [custom_statements](#sqlite-custom_statements) + * statement + * [references](#sqlite-references) + * reference + + +### Examples +``` +sqlite do + repo MyApp.Repo + table "organizations" +end + +``` + + + + +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`repo`](#sqlite-repo){: #sqlite-repo .spark-required} | `atom` | | The repo that will be used to fetch your data. See the `AshSqlite.Repo` documentation for more | +| [`migrate?`](#sqlite-migrate?){: #sqlite-migrate? } | `boolean` | `true` | Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations` | +| [`migration_types`](#sqlite-migration_types){: #sqlite-migration_types } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration type that should be used for that attribute. Only necessary if you need to override the defaults. | +| [`migration_defaults`](#sqlite-migration_defaults){: #sqlite-migration_defaults } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration default that should be used for that attribute. The string you use will be placed verbatim in the migration. Use fragments like `fragment(\\"now()\\")`, or for `nil`, use `\\"nil\\"`. | +| [`base_filter_sql`](#sqlite-base_filter_sql){: #sqlite-base_filter_sql } | `String.t` | | A raw sql version of the base_filter, e.g `representative = true`. Required if trying to create a unique constraint on a resource with a base_filter | +| [`skip_unique_indexes`](#sqlite-skip_unique_indexes){: #sqlite-skip_unique_indexes } | `atom \| list(atom)` | `false` | Skip generating unique indexes when generating migrations | +| [`unique_index_names`](#sqlite-unique_index_names){: #sqlite-unique_index_names } | `list({list(atom), String.t} \| {list(atom), String.t, String.t})` | `[]` | A list of unique index names that could raise errors that are not configured in identities, or an mfa to a function that takes a changeset and returns the list. In the format `{[:affected, :keys], "name_of_constraint"}` or `{[:affected, :keys], "name_of_constraint", "custom error message"}` | +| [`exclusion_constraint_names`](#sqlite-exclusion_constraint_names){: #sqlite-exclusion_constraint_names } | `any` | `[]` | A list of exclusion constraint names that could raise errors. Must be in the format `{:affected_key, "name_of_constraint"}` or `{:affected_key, "name_of_constraint", "custom error message"}` | +| [`identity_index_names`](#sqlite-identity_index_names){: #sqlite-identity_index_names } | `any` | `[]` | A keyword list of identity names to the unique index name that they should use when being managed by the migration generator. | +| [`foreign_key_names`](#sqlite-foreign_key_names){: #sqlite-foreign_key_names } | `list({atom, String.t} \| {String.t, String.t})` | `[]` | A list of foreign keys that could raise errors, or an mfa to a function that takes a changeset and returns a list. In the format: `{:key, "name_of_constraint"}` or `{:key, "name_of_constraint", "custom error message"}` | +| [`migration_ignore_attributes`](#sqlite-migration_ignore_attributes){: #sqlite-migration_ignore_attributes } | `list(atom)` | `[]` | A list of attributes that will be ignored when generating migrations. | +| [`table`](#sqlite-table){: #sqlite-table } | `String.t` | | The table to store and read the resource from. If this is changed, the migration generator will not remove the old table. | +| [`polymorphic?`](#sqlite-polymorphic?){: #sqlite-polymorphic? } | `boolean` | `false` | Declares this resource as polymorphic. See the [polymorphic resources guide](/documentation/topics/resources/polymorphic-resources.md) for more. | + + +## sqlite.custom_indexes +A section for configuring indexes to be created by the migration generator. + +In general, prefer to use `identities` for simple unique constraints. This is a tool to allow +for declaring more complex indexes. + + +### Nested DSLs + * [index](#sqlite-custom_indexes-index) + + +### Examples +``` +custom_indexes do + index [:column1, :column2], unique: true, where: "thing = TRUE" +end + +``` + + + + +## sqlite.custom_indexes.index +```elixir +index fields +``` + + +Add an index to be managed by the migration generator. + + + + +### Examples +``` +index ["column", "column2"], unique: true, where: "thing = TRUE" +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`fields`](#sqlite-custom_indexes-index-fields){: #sqlite-custom_indexes-index-fields } | `atom \| String.t \| list(atom \| String.t)` | | The fields to include in the index. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`name`](#sqlite-custom_indexes-index-name){: #sqlite-custom_indexes-index-name } | `String.t` | | the name of the index. Defaults to "#{table}_#{column}_index". | +| [`unique`](#sqlite-custom_indexes-index-unique){: #sqlite-custom_indexes-index-unique } | `boolean` | `false` | indicates whether the index should be unique. | +| [`using`](#sqlite-custom_indexes-index-using){: #sqlite-custom_indexes-index-using } | `String.t` | | configures the index type. | +| [`where`](#sqlite-custom_indexes-index-where){: #sqlite-custom_indexes-index-where } | `String.t` | | specify conditions for a partial index. | +| [`message`](#sqlite-custom_indexes-index-message){: #sqlite-custom_indexes-index-message } | `String.t` | | A custom message to use for unique indexes that have been violated | +| [`include`](#sqlite-custom_indexes-index-include){: #sqlite-custom_indexes-index-include } | `list(String.t)` | | specify fields for a covering index. This is not supported by all databases. For more information on SQLite support, please read the official docs. | + + + + + +### Introspection + +Target: `AshSqlite.CustomIndex` + + +## sqlite.custom_statements +A section for configuring custom statements to be added to migrations. + +Changing custom statements may require manual intervention, because Ash can't determine what order they should run +in (i.e if they depend on table structure that you've added, or vice versa). As such, any `down` statements we run +for custom statements happen first, and any `up` statements happen last. + +Additionally, when changing a custom statement, we must make some assumptions, i.e that we should migrate +the old structure down using the previously configured `down` and recreate it. + +This may not be desired, and so what you may end up doing is simply modifying the old migration and deleting whatever was +generated by the migration generator. As always: read your migrations after generating them! + + +### Nested DSLs + * [statement](#sqlite-custom_statements-statement) + + +### Examples +``` +custom_statements do + # the name is used to detect if you remove or modify the statement + statement :pgweb_idx do + up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" + down "DROP INDEX pgweb_idx;" + end +end + +``` + + + + +## sqlite.custom_statements.statement +```elixir +statement name +``` + + +Add a custom statement for migrations. + + + + +### Examples +``` +statement :pgweb_idx do + up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" + down "DROP INDEX pgweb_idx;" +end + +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`name`](#sqlite-custom_statements-statement-name){: #sqlite-custom_statements-statement-name .spark-required} | `atom` | | The name of the statement, must be unique within the resource | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`up`](#sqlite-custom_statements-statement-up){: #sqlite-custom_statements-statement-up .spark-required} | `String.t` | | How to create the structure of the statement | +| [`down`](#sqlite-custom_statements-statement-down){: #sqlite-custom_statements-statement-down .spark-required} | `String.t` | | How to tear down the structure of the statement | +| [`code?`](#sqlite-custom_statements-statement-code?){: #sqlite-custom_statements-statement-code? } | `boolean` | `false` | By default, we place the strings inside of ecto migration's `execute/1` function and assume they are sql. Use this option if you want to provide custom elixir code to be placed directly in the migrations | + + + + + +### Introspection + +Target: `AshSqlite.Statement` + + +## sqlite.references +A section for configuring the references (foreign keys) in resource migrations. + +This section is only relevant if you are using the migration generator with this resource. +Otherwise, it has no effect. + + +### Nested DSLs + * [reference](#sqlite-references-reference) + + +### Examples +``` +references do + reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey" +end + +``` + + + + +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`polymorphic_on_delete`](#sqlite-references-polymorphic_on_delete){: #sqlite-references-polymorphic_on_delete } | `:delete \| :nilify \| :nothing \| :restrict` | | For polymorphic resources, configures the on_delete behavior of the automatically generated foreign keys to source tables. | +| [`polymorphic_on_update`](#sqlite-references-polymorphic_on_update){: #sqlite-references-polymorphic_on_update } | `:update \| :nilify \| :nothing \| :restrict` | | For polymorphic resources, configures the on_update behavior of the automatically generated foreign keys to source tables. | +| [`polymorphic_name`](#sqlite-references-polymorphic_name){: #sqlite-references-polymorphic_name } | `:update \| :nilify \| :nothing \| :restrict` | | For polymorphic resources, configures the on_update behavior of the automatically generated foreign keys to source tables. | + + + +## sqlite.references.reference +```elixir +reference relationship +``` + + +Configures the reference for a relationship in resource migrations. + +Keep in mind that multiple relationships can theoretically involve the same destination and foreign keys. +In those cases, you only need to configure the `reference` behavior for one of them. Any conflicts will result +in an error, across this resource and any other resources that share a table with this one. For this reason, +instead of adding a reference configuration for `:nothing`, its best to just leave the configuration out, as that +is the default behavior if *no* relationship anywhere has configured the behavior of that reference. + + + + +### Examples +``` +reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey" +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`relationship`](#sqlite-references-reference-relationship){: #sqlite-references-reference-relationship .spark-required} | `atom` | | The relationship to be configured | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`ignore?`](#sqlite-references-reference-ignore?){: #sqlite-references-reference-ignore? } | `boolean` | | If set to true, no reference is created for the given relationship. This is useful if you need to define it in some custom way | +| [`on_delete`](#sqlite-references-reference-on_delete){: #sqlite-references-reference-on_delete } | `:delete \| :nilify \| :nothing \| :restrict` | | What should happen to records of this resource when the referenced record of the *destination* resource is deleted. | +| [`on_update`](#sqlite-references-reference-on_update){: #sqlite-references-reference-on_update } | `:update \| :nilify \| :nothing \| :restrict` | | What should happen to records of this resource when the referenced destination_attribute of the *destination* record is update. | +| [`deferrable`](#sqlite-references-reference-deferrable){: #sqlite-references-reference-deferrable } | `false \| true \| :initially` | `false` | Wether or not the constraint is deferrable. This only affects the migration generator. | +| [`name`](#sqlite-references-reference-name){: #sqlite-references-reference-name } | `String.t` | | The name of the foreign key to generate in the database. Defaults to __fkey | + + + + + +### Introspection + +Target: `AshSqlite.Reference` + + + + + + + + diff --git a/documentation/topics/about-ash-sqlite/what-is-ash-sqlite.md b/documentation/topics/about-ash-sqlite/what-is-ash-sqlite.md new file mode 100644 index 0000000..b77be02 --- /dev/null +++ b/documentation/topics/about-ash-sqlite/what-is-ash-sqlite.md @@ -0,0 +1,34 @@ +# What is AshSqlite? + +AshSqlite is the SQLite `Ash.DataLayer` for [Ash Framework](https://hexdocs.pm/ash). This doesn't have all of the features of [AshPostgres](https://hexdocs.pm/ash_postgres), but it does support most of the features of Ash data layers. The main feature missing is Aggregate support. + +Use this to persist records in a SQLite table. For example, the resource below would be persisted in a table called `tweets`: + +```elixir +defmodule MyApp.Tweet do + use Ash.Resource, + data_layer: AshSQLite.DataLayer + + attributes do + integer_primary_key :id + attribute :text, :string + end + + relationships do + belongs_to :author, MyApp.User + end + + sqlite do + table "tweets" + repo MyApp.Repo + end +end +``` + +The table might look like this: + +| id | text | author_id | +| --- | --------------- | --------- | +| 1 | "Hello, world!" | 1 | + +Creating records would add to the table, destroying records would remove from the table, and updating records would update the table. diff --git a/documentation/topics/advanced/expressions.md b/documentation/topics/advanced/expressions.md new file mode 100644 index 0000000..f377eed --- /dev/null +++ b/documentation/topics/advanced/expressions.md @@ -0,0 +1,61 @@ +# Expressions + +In addition to the expressions listed in the [Ash expressions guide](https://hexdocs.pm/ash/expressions.html), AshSqlite provides the following expressions + +# Fragments + +Fragments allow you to use arbitrary sqlite expressions in your queries. Fragments can often be an escape hatch to allow you to do things that don't have something officially supported with Ash. + +### Examples + +#### Simple expressions + +```elixir +fragment("? / ?", points, count) +``` + +#### Calling functions + +```elixir +fragment("repeat('hello', 4)") +``` + +#### Using entire queries + +```elixir +fragment("points > (SELECT SUM(points) FROM games WHERE user_id = ? AND id != ?)", user_id, id) +``` + +> ### a last resport {: .warning} +> +> Using entire queries as shown above is a last resort, but can sometimes be the best way to accomplish a given task. + +#### In calculations + +```elixir +calculations do + calculate :lower_name, :string, expr( + fragment("LOWER(?)", name) + ) +end +``` + +#### In migrations + +```elixir +create table(:managers, primary_key: false) do + add :id, :uuid, null: false, default: fragment("UUID_GENERATE_V4()"), primary_key: true +end +``` + +## Like + +These wrap the sqlite builtin like operator + +Please be aware, these match _patterns_ not raw text. Use `contains/1` if you want to match text without supporting patterns, i.e `%` and `_` have semantic meaning! + +For example: + +```elixir +Ash.Query.filter(User, like(name, "%obo%")) # name contains obo anywhere in the string, case sensitively +``` diff --git a/documentation/topics/advanced/manual-relationships.md b/documentation/topics/advanced/manual-relationships.md new file mode 100644 index 0000000..ad431cf --- /dev/null +++ b/documentation/topics/advanced/manual-relationships.md @@ -0,0 +1,87 @@ +# Join Manual Relationships + +See [Defining Manual Relationships](https://hexdocs.pm/ash/defining-manual-relationships.html) for an idea of manual relationships in general. +Manual relationships allow for expressing complex/non-typical relationships between resources in a standard way. +Individual data layers may interact with manual relationships in their own way, so see their corresponding guides. + +## Example + +```elixir +# in the resource + +relationships do + has_many :tickets_above_threshold, Helpdesk.Support.Ticket do + manual Helpdesk.Support.Ticket.Relationships.TicketsAboveThreshold + end +end + +# implementation +defmodule Helpdesk.Support.Ticket.Relationships.TicketsAboveThreshold do + use Ash.Resource.ManualRelationship + use AshSqlite.ManualRelationship + + require Ash.Query + require Ecto.Query + + def load(records, _opts, %{query: query, actor: actor, authorize?: authorize?}) do + # Use existing records to limit resultds + rep_ids = Enum.map(records, & &1.id) + # Using Ash to get the destination records is ideal, so you can authorize access like normal + # but if you need to use a raw ecto query here, you can. As long as you return the right structure. + + {:ok, + query + |> Ash.Query.filter(representative_id in ^rep_ids) + |> Ash.Query.filter(priority > representative.priority_threshold) + |> Helpdesk.Support.read!(actor: actor, authorize?: authorize?) + # Return the items grouped by the primary key of the source, i.e representative.id => [...tickets above threshold] + |> Enum.group_by(& &1.representative_id)} + end + + # query is the "source" query that is being built. + + # _opts are options provided to the manual relationship, i.e `{Manual, opt: :val}` + + # current_binding is what the source of the relationship is bound to. Access fields with `as(^current_binding).field` + + # as_binding is the binding that your join should create. When you join, make sure you say `as: ^as_binding` on the + # part of the query that represents the destination of the relationship + + # type is `:inner` or `:left`. + # destination_query is what you should join to to add the destination to the query, i.e `join: dest in ^destination-query` + def ash_sqlite_join(query, _opts, current_binding, as_binding, :inner, destination_query) do + {:ok, + Ecto.Query.from(_ in query, + join: dest in ^destination_query, + as: ^as_binding, + on: dest.representative_id == as(^current_binding).id, + on: dest.priority > as(^current_binding).priority_threshold + )} + end + + def ash_sqlite_join(query, _opts, current_binding, as_binding, :left, destination_query) do + {:ok, + Ecto.Query.from(_ in query, + left_join: dest in ^destination_query, + as: ^as_binding, + on: dest.representative_id == as(^current_binding).id, + on: dest.priority > as(^current_binding).priority_threshold + )} + end + + # _opts are options provided to the manual relationship, i.e `{Manual, opt: :val}` + + # current_binding is what the source of the relationship is bound to. Access fields with `parent_as(^current_binding).field` + + # as_binding is the binding that has already been created for your join. Access fields on it via `as(^as_binding)` + + # destination_query is what you should use as the basis of your query + def ash_sqlite_subquery(_opts, current_binding, as_binding, destination_query) do + {:ok, + Ecto.Query.from(_ in destination_query, + where: parent_as(^current_binding).id == as(^as_binding).representative_id, + where: as(^as_binding).priority > parent_as(^current_binding).priority_threshold + )} + end +end +``` diff --git a/documentation/topics/development/migrations-and-tasks.md b/documentation/topics/development/migrations-and-tasks.md new file mode 100644 index 0000000..af9cd83 --- /dev/null +++ b/documentation/topics/development/migrations-and-tasks.md @@ -0,0 +1,98 @@ +# Migrations + +## Tasks + +Ash comes with its own tasks, and AshSqlite exposes lower level tasks that you can use if necessary. This guide shows the process using `ash.*` tasks, and the `ash_sqlite.*` tasks are illustrated at the bottom. + +## Basic W## Basic Workflow + +- Make resource changes +- Run `mix ash.codegen --name add_a_combobulator` to generate migrations and resource snapshots +- Run `mix ash.migrate` to run those migrations + +For more information on generating migrations, run `mix help ash_sqlite.generate_migrations` (the underlying task that is called by `mix ash.migrate`) + +### Regenerating Migratio### Regenerating Migrations + +Often, you will run into a situation where you want to make a slight change to a resource after you've already generated and run migrations. If you are using git and would like to undo those changes, then regenerate the migrations, this script may prove useful: + +```bash +#!/bin/bash + +# Get count of untracked migrations +N_MIGRATIONS=$(git ls-files --others priv/repo/migrations | wc -l) + +# Rollback untracked migrations +mix ash_sqlite.rollback -n $N_MIGRATIONS + +# Delete untracked migrations and snapshots +git ls-files --others priv/repo/migrations | xargs rm +git ls-files --others priv/resource_snapshots | xargs rm + +# Regenerate migrations +mix ash.codegen --name $1 + +# Run migrations if flag +if echo $* | grep -e "-m" -q +then + mix ash.migrate +fi +``` + +After saving this file to something like `regen.sh`, make it executable with `chmod +x regen.sh`. Now you can run it with `./regen.sh name_of_operation`. If you would like the migrations to automatically run after regeneration, add the `-m` flag: `./regen.sh name_of_operation -m`. + +## Multiple Repos + +If you are using multiple repos, you will likely need to use `mix ecto.migrate` and manage it separately for each repo, as the options would +be applied to both repo, which wouldn't make sense. + +## Running Migrations in Production + +Define a module similar to the following: + +```elixir +defmodule MyApp.Release do + @moduledoc """ + Houses tasks that need to be executed in the released application (because mix is not present in releases). + """ + @app :my_ap + def migrate do + load_app() + + for repo <- repos() do + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) + end + end + + def rollback(repo, version) do + load_app() + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) + end + + defp repos do + domains() + |> Enum.flat_map(fn domain -> + domain + |> Ash.Domain.Info.resources() + |> Enum.map(&AshSqlite.repo/1) + end) + |> Enum.uniq() + end + + defp domains do + Application.fetch_env!(:my_app, :ash_domains) + end + + defp load_app do + Application.load(@app) + end +end +``` + +# AshSqlite-specific tasks + +- `mix ash_sqlite.generate_migrations` +- `mix ash_sqlite.create` +- `mix ash_sqlite.migrate` +- `mix ash_sqlite.rollback` +- `mix ash_sqlite.drop` diff --git a/documentation/topics/development/testing.md b/documentation/topics/development/testing.md new file mode 100644 index 0000000..772c887 --- /dev/null +++ b/documentation/topics/development/testing.md @@ -0,0 +1,11 @@ +# Testing With Sqlite + +Testing resources with SQLite generally requires passing `async?: false` to +your tests, due to `SQLite`'s limitation of having a single write transaction +open at any one time. + +This should be coupled with to make sure that Ash does not spawn any tasks. + +```elixir +config :ash, :disable_async?, true +``` diff --git a/documentation/topics/resources/polymorphic-resources.md b/documentation/topics/resources/polymorphic-resources.md new file mode 100644 index 0000000..2616fe9 --- /dev/null +++ b/documentation/topics/resources/polymorphic-resources.md @@ -0,0 +1,85 @@ +# Polymorphic Resources + +To support leveraging the same resource backed by multiple tables (useful for things like polymorphic associations), AshSqlite supports setting the `data_layer.table` context for a given resource. For this example, lets assume that you have a `MyApp.Post` resource and a `MyApp.Comment` resource. For each of those resources, users can submit `reactions`. However, you want a separate table for `post_reactions` and `comment_reactions`. You could accomplish that like so: + +```elixir +defmodule MyApp.Reaction do + use Ash.Resource, + domain: MyApp.Domain, + data_layer: AshSqlite.DataLayer + + sqlite do + polymorphic? true # Without this, `table` is a required configuration + end + + attributes do + attribute(:resource_id, :uuid) + end + + ... +end +``` + +Then, in your related resources, you set the table context like so: + +```elixir +defmodule MyApp.Post do + use Ash.Resource, + domain: MyApp.Domain, + data_layer: AshSqlite.DataLayer + + ... + + relationships do + has_many :reactions, MyApp.Reaction, + relationship_context: %{data_layer: %{table: "post_reactions"}}, + destination_attribute: :resource_id + end +end + +defmodule MyApp.Comment do + use Ash.Resource, + domain: MyApp.Domain, + data_layer: AshSqlite.DataLayer + + ... + + relationships do + has_many :reactions, MyApp.Reaction, + relationship_context: %{data_layer: %{table: "comment_reactions"}}, + destination_attribute: :resource_id + end +end +``` + +With this, when loading or editing related data, ash will automatically set that context. +For managing related data, see `Ash.Changeset.manage_relationship/4` and other relationship functions +in `Ash.Changeset` + +## Table specific actions + +To make actions use a specific table, you can use the `set_context` query preparation/change. + +For example: + +```elixir +defmodule MyApp.Reaction do + actions do + read :for_comments do + prepare set_context(%{data_layer: %{table: "comment_reactions"}}) + end + + read :for_posts do + prepare set_context(%{data_layer: %{table: "post_reactions"}}) + end + end +end +``` + +## Migrations + +When a migration is marked as `polymorphic? true`, the migration generator will look at +all resources that are related to it, that set the `%{data_layer: %{table: "table"}}` context. +For each of those, a migration is generated/managed automatically. This means that adding reactions +to a new resource is as easy as adding the relationship and table context, and then running +`mix ash_sqlite.generate_migrations`. diff --git a/documentation/topics/resources/references.md b/documentation/topics/resources/references.md new file mode 100644 index 0000000..ceecb61 --- /dev/null +++ b/documentation/topics/resources/references.md @@ -0,0 +1,23 @@ +# References + +To configure the foreign keys on a resource, we use the `references` block. + +For example: + +```elixir +references do + reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey" +end +``` + +## Important + +No resource logic is applied with these operations! No authorization rules or validations take place, and no notifications are issued. This operation happens *directly* in the database. That + +## Nothing vs Restrict + +The difference between `:nothing` and `:restrict` is subtle and, if you are unsure, choose `:nothing` (the default behavior). `:restrict` will prevent the deletion from happening *before* the end of the database transaction, whereas `:nothing` allows the transaction to complete before doing so. This allows for things like updating or deleting the destination row and *then* updating updating or deleting the reference(as long as you are in a transaction). + +## On Delete + +This option is called `on_delete`, instead of `on_destroy`, because it is hooking into the database level deletion, *not* a `destroy` action in your resource. diff --git a/documentation/tutorials/getting-started-with-ash-sqlite.md b/documentation/tutorials/getting-started-with-ash-sqlite.md new file mode 100644 index 0000000..fc428bc --- /dev/null +++ b/documentation/tutorials/getting-started-with-ash-sqlite.md @@ -0,0 +1,277 @@ +# Getting Started With AshSqlite + +## Goals + +In this guide we will: + +1. Setup AshSqlite, which includes setting up [Ecto](https://hexdocs.pm/ecto/Ecto.html) +2. Add AshSqlite to the resources created in [the Ash getting started guide](https://hexdocs.pm/ash/get-started.html) +3. Show how the various features of AshSqlite can help you work quickly and cleanly against a sqlite database +4. Highlight some of the more advanced features you can use when using AshSqlite. +5. Point you to additional resources you may need on your journey + +## Requirements + +- A working SQLite installation, with a sufficiently permissive user +- If you would like to follow along, you will need to add begin with [the Ash getting started guide](https://hexdocs.pm/ash/get-started.html) + +## Steps + +### Add AshSqlite + +Add the `:ash_sqlite` dependency to your application + +`{:ash_sqlite, "~> 0.1.2"}` + +Add `:ash_sqlite` to your `.formatter.exs` file + +```elixir +[ + # import the formatter rules from `:ash_sqlite` + import_deps: [..., :ash_sqlite], + inputs: [...] +] +``` + +### Create and configure your Repo + +Create `lib/helpdesk/repo.ex` with the following contents. `AshSqlite.Repo` is a thin wrapper around `Ecto.Repo`, so see their documentation for how to use it if you need to use it directly. For standard Ash usage, all you will need to do is configure your resources to use your repo. + +```elixir +# in lib/helpdesk/repo.ex + +defmodule Helpdesk.Repo do + use AshSqlite.Repo, otp_app: :helpdesk +end +``` + +Next we will need to create configuration files for various environments. Run the following to create the configuration files we need. + +```bash +mkdir -p config +touch config/config.exs +touch config/dev.exs +touch config/runtime.exs +touch config/test.exs +``` + +Place the following contents in those files, ensuring that the credentials match the user you created for your database. For most conventional installations this will work out of the box. If you've followed other guides before this one, they may have had you create these files already, so just make sure these contents are there. + +```elixir +# in config/config.exs +import Config + +# This should already have been added in the first +# getting started guide +config :helpdesk, + ash_apis: [Helpdesk.Support] + +config :helpdesk, + ecto_repos: [Helpdesk.Repo] + +# Import environment specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +import_config "#{config_env()}.exs" +``` + +```elixir +# in config/dev.exs + +import Config + +# Configure your database +config :helpdesk, Helpdesk.Repo, + database: Path.join(__DIR__, "../path/to/your.db"), + port: 5432, + show_sensitive_data_on_connection_error: true, + pool_size: 10 +``` + +```elixir +# in config/runtime.exs + +import Config + +if config_env() == :prod do + config :helpdesk, Helpdesk.Repo, + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") +end +``` + +```elixir +# in config/test.exs + +import Config + +# Configure your database +# +# The MIX_TEST_PARTITION environment variable can be used +# to provide built-in test partitioning in CI environment. +# Run `mix help test` for more information. +config :helpdesk, Helpdesk.Repo, + database: Path.join(__DIR__, "../path/to/your#{System.get_env("MIX_TEST_PARTITION")}.db"), + pool_size: 10 +``` + +And finally, add the repo to your application + +```elixir +# in lib/helpdesk/application.ex + + def start(_type, _args) do + children = [ + # Starts a worker by calling: Helpdesk.Worker.start_link(arg) + # {Helpdesk.Worker, arg} + Helpdesk.Repo + ] + + ... +``` + +### Add AshSqlite to our resources + +Now we can add the data layer to our resources. The basic configuration for a resource requires the `d:AshSqlite.sqlite|table` and the `d:AshSqlite.sqlite|repo`. + +```elixir +# in lib/helpdesk/support/resources/ticket.ex + + use Ash.Resource, + domain: MyApp.Domain, + data_layer: AshSqlite.DataLayer + + sqlite do + table "tickets" + repo Helpdesk.Repo + end +``` + +```elixir +# in lib/helpdesk/support/resources/representative.ex + + use Ash.Resource, + domain: MyApp.Domain, + data_layer: AshSqlite.DataLayer + + sqlite do + table "representatives" + repo Helpdesk.Repo + end +``` + +### Create the database and tables + +First, we'll create the database with `mix ash_sqlite.create`. + +Then we will generate database migrations. This is one of the many ways that AshSqlite can save time and reduce complexity. + +```bash +mix ash_sqlite.generate_migrations --name add_tickets_and_representatives +``` + +If you are unfamiliar with database migrations, it is a good idea to get a rough idea of what they are and how they work. See the links at the bottom of this guide for more. A rough overview of how migrations work is that each time you need to make changes to your database, they are saved as small, reproducible scripts that can be applied in order. This is necessary both for clean deploys as well as working with multiple developers making changes to the structure of a single database. + +Typically, you need to write these by hand. AshSqlite, however, will store snapshots each time you run the command to generate migrations and will figure out what migrations need to be created. + +You should always look at the generated migrations to ensure that they look correct. Do so now by looking at the generated file in `priv/repo/migrations`. + +Finally, we will create the local database and apply the generated migrations: + +```bash +mix ash_sqlite.create +mix ash_sqlite.migrate +``` + +### Try it out + +And now we're ready to try it out! Run the following in iex: + +Lets create some data. We'll make a representative and give them some open and some closed tickets. + +```elixir +require Ash.Query + +representative = ( + Helpdesk.Support.Representative + |> Ash.Changeset.for_create(:create, %{name: "Joe Armstrong"}) + |> Helpdesk.Support.create!() +) + +for i <- 0..5 do + ticket = + Helpdesk.Support.Ticket + |> Ash.Changeset.for_create(:open, %{subject: "Issue #{i}"}) + |> Helpdesk.Support.create!() + |> Ash.Changeset.for_update(:assign, %{representative_id: representative.id}) + |> Helpdesk.Support.update!() + + if rem(i, 2) == 0 do + ticket + |> Ash.Changeset.for_update(:close) + |> Helpdesk.Support.update!() + end +end +``` + +And now we can read that data. You should see some debug logs that show the sql queries AshSqlite is generating. + +```elixir +require Ash.Query + +# Show the tickets where the subject equals "foobar" +Helpdesk.Support.Ticket +|> Ash.Query.filter(subject == "foobar") +|> Helpdesk.Support.read!() +``` + +```elixir +require Ash.Query + +# Show the tickets that are closed and their subject does not equal "barbaz" +Helpdesk.Support.Ticket +|> Ash.Query.filter(status == :closed and not(subject == "barbaz")) +|> Helpdesk.Support.read!() +``` + +And, naturally, now that we are storing this in sqlite, this database is persisted even if we stop/start our application. The nice thing, however, is that this was the _exact_ same code that we ran against our resources when they were backed by ETS. + +### Calculations + +Calculations can be pushed down into SQL using expressions. + +For example, we can determine the percentage of tickets that are open: + +```elixir +# in lib/helpdesk/support/resources/representative.ex + + calculations do + calculate :percent_open, :float, expr(open_tickets / total_tickets ) + end +``` + +Calculations can be loaded. + +```elixir +require Ash.Query + +Helpdesk.Support.Representative +|> Ash.Query.filter(percent_open > 0.25) +|> Ash.Query.sort(:percent_open) +|> Ash.Query.load(:percent_open) +|> Helpdesk.Support.read!() +``` + +### Rich Configuration Options + +Take a look at the DSL documentation for more information on what you can configure. You can add check constraints, configure the behavior of foreign keys and more! + +### Deployment + +When deploying, you will need to ensure that the file you are using in production is persisted in some way (probably, unless you want it to disappear whenever your deployed system restarts). For example with fly.io this might mean adding a volume to your deployment. + +### What next? + +- Check out the data layer docs: `AshSqlite.DataLayer` + +- [Ecto's documentation](https://hexdocs.pm/ecto/Ecto.html). AshSqlite (and much of Ash itself) is made possible by the amazing Ecto. If you find yourself looking for escape hatches when using Ash or ways to work directly with your database, you will want to know how Ecto works. Ash and AshSqlite intentionally do not hide Ecto, and in fact encourages its use whenever you need an escape hatch. + +- [Ecto's Migration documentation](https://hexdocs.pm/ecto_sql/Ecto.Migration.html) read more about migrations. Even with the ash_sqlite migration generator, you will very likely need to modify your own migrations some day. diff --git a/lib/ash_sqlite.ex b/lib/ash_sqlite.ex new file mode 100644 index 0000000..e09956f --- /dev/null +++ b/lib/ash_sqlite.ex @@ -0,0 +1,7 @@ +defmodule AshSqlite do + @moduledoc """ + The AshSqlite extension gives you tools to map a resource to a sqlite database table. + + For more, check out the [getting started guide](/documentation/tutorials/getting-started-with-ash-sqlite.md) + """ +end diff --git a/lib/custom_extension.ex b/lib/custom_extension.ex new file mode 100644 index 0000000..62cdaad --- /dev/null +++ b/lib/custom_extension.ex @@ -0,0 +1,20 @@ +defmodule AshSqlite.CustomExtension do + @moduledoc """ + A custom extension implementation. + """ + + @callback install(version :: integer) :: String.t() + + @callback uninstall(version :: integer) :: String.t() + + defmacro __using__(name: name, latest_version: latest_version) do + quote do + @behaviour AshSqlite.CustomExtension + + @extension_name unquote(name) + @extension_latest_version unquote(latest_version) + + def extension, do: {@extension_name, @extension_latest_version, &install/1, &uninstall/1} + end + end +end diff --git a/lib/custom_index.ex b/lib/custom_index.ex new file mode 100644 index 0000000..49bb093 --- /dev/null +++ b/lib/custom_index.ex @@ -0,0 +1,109 @@ +defmodule AshSqlite.CustomIndex do + @moduledoc "Represents a custom index on the table backing a resource" + @fields [ + :table, + :fields, + :name, + :unique, + :using, + :where, + :include, + :message + ] + + defstruct @fields + + def fields, do: @fields + + @schema [ + fields: [ + type: {:wrap_list, {:or, [:atom, :string]}}, + doc: "The fields to include in the index." + ], + name: [ + type: :string, + doc: "the name of the index. Defaults to \"\#\{table\}_\#\{column\}_index\"." + ], + unique: [ + type: :boolean, + doc: "indicates whether the index should be unique.", + default: false + ], + using: [ + type: :string, + doc: "configures the index type." + ], + where: [ + type: :string, + doc: "specify conditions for a partial index." + ], + message: [ + type: :string, + doc: "A custom message to use for unique indexes that have been violated" + ], + include: [ + type: {:list, :string}, + doc: + "specify fields for a covering index. This is not supported by all databases. For more information on SQLite support, please read the official docs." + ] + ] + + def schema, do: @schema + + # sobelow_skip ["DOS.StringToAtom"] + def transform(%__MODULE__{fields: fields} = index) do + index = %{ + index + | fields: + Enum.map(fields, fn field -> + if is_atom(field) do + field + else + String.to_atom(field) + end + end) + } + + cond do + index.name -> + if Regex.match?(~r/^[0-9a-zA-Z_]+$/, index.name) do + {:ok, index} + else + {:error, + "Custom index name #{index.name} is not valid. Must have letters, numbers and underscores only"} + end + + mismatched_field = + Enum.find(index.fields, fn field -> + !Regex.match?(~r/^[0-9a-zA-Z_]+$/, to_string(field)) + end) -> + {:error, + """ + Custom index field #{mismatched_field} contains invalid index name characters. + + A name must be set manually, i.e + + `name: "your_desired_index_name"` + + Index names must have letters, numbers and underscores only + """} + + true -> + {:ok, index} + end + end + + def name(_resource, %{name: name}) when is_binary(name) do + name + end + + # sobelow_skip ["DOS.StringToAtom"] + def name(table, %{fields: fields}) do + [table, fields, "index"] + |> List.flatten() + |> Enum.map(&to_string(&1)) + |> Enum.map(&String.replace(&1, ~r"[^\w_]", "_")) + |> Enum.map_join("_", &String.replace_trailing(&1, "_", "")) + |> String.to_atom() + end +end diff --git a/lib/data_layer.ex b/lib/data_layer.ex new file mode 100644 index 0000000..3502fcc --- /dev/null +++ b/lib/data_layer.ex @@ -0,0 +1,1589 @@ +defmodule AshSqlite.DataLayer do + @index %Spark.Dsl.Entity{ + name: :index, + describe: """ + Add an index to be managed by the migration generator. + """, + examples: [ + "index [\"column\", \"column2\"], unique: true, where: \"thing = TRUE\"" + ], + target: AshSqlite.CustomIndex, + schema: AshSqlite.CustomIndex.schema(), + transform: {AshSqlite.CustomIndex, :transform, []}, + args: [:fields] + } + + @custom_indexes %Spark.Dsl.Section{ + name: :custom_indexes, + describe: """ + A section for configuring indexes to be created by the migration generator. + + In general, prefer to use `identities` for simple unique constraints. This is a tool to allow + for declaring more complex indexes. + """, + examples: [ + """ + custom_indexes do + index [:column1, :column2], unique: true, where: "thing = TRUE" + end + """ + ], + entities: [ + @index + ] + } + + @statement %Spark.Dsl.Entity{ + name: :statement, + describe: """ + Add a custom statement for migrations. + """, + examples: [ + """ + statement :pgweb_idx do + up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" + down "DROP INDEX pgweb_idx;" + end + """ + ], + target: AshSqlite.Statement, + schema: AshSqlite.Statement.schema(), + args: [:name] + } + + @custom_statements %Spark.Dsl.Section{ + name: :custom_statements, + describe: """ + A section for configuring custom statements to be added to migrations. + + Changing custom statements may require manual intervention, because Ash can't determine what order they should run + in (i.e if they depend on table structure that you've added, or vice versa). As such, any `down` statements we run + for custom statements happen first, and any `up` statements happen last. + + Additionally, when changing a custom statement, we must make some assumptions, i.e that we should migrate + the old structure down using the previously configured `down` and recreate it. + + This may not be desired, and so what you may end up doing is simply modifying the old migration and deleting whatever was + generated by the migration generator. As always: read your migrations after generating them! + """, + examples: [ + """ + custom_statements do + # the name is used to detect if you remove or modify the statement + statement :pgweb_idx do + up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" + down "DROP INDEX pgweb_idx;" + end + end + """ + ], + entities: [ + @statement + ] + } + + @reference %Spark.Dsl.Entity{ + name: :reference, + describe: """ + Configures the reference for a relationship in resource migrations. + + Keep in mind that multiple relationships can theoretically involve the same destination and foreign keys. + In those cases, you only need to configure the `reference` behavior for one of them. Any conflicts will result + in an error, across this resource and any other resources that share a table with this one. For this reason, + instead of adding a reference configuration for `:nothing`, its best to just leave the configuration out, as that + is the default behavior if *no* relationship anywhere has configured the behavior of that reference. + """, + examples: [ + "reference :post, on_delete: :delete, on_update: :update, name: \"comments_to_posts_fkey\"" + ], + args: [:relationship], + target: AshSqlite.Reference, + schema: AshSqlite.Reference.schema() + } + + @references %Spark.Dsl.Section{ + name: :references, + describe: """ + A section for configuring the references (foreign keys) in resource migrations. + + This section is only relevant if you are using the migration generator with this resource. + Otherwise, it has no effect. + """, + examples: [ + """ + references do + reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey" + end + """ + ], + entities: [@reference], + schema: [ + polymorphic_on_delete: [ + type: {:one_of, [:delete, :nilify, :nothing, :restrict]}, + doc: + "For polymorphic resources, configures the on_delete behavior of the automatically generated foreign keys to source tables." + ], + polymorphic_on_update: [ + type: {:one_of, [:update, :nilify, :nothing, :restrict]}, + doc: + "For polymorphic resources, configures the on_update behavior of the automatically generated foreign keys to source tables." + ], + polymorphic_name: [ + type: {:one_of, [:update, :nilify, :nothing, :restrict]}, + doc: + "For polymorphic resources, configures the on_update behavior of the automatically generated foreign keys to source tables." + ] + ] + } + + @references %Spark.Dsl.Section{ + name: :references, + describe: """ + A section for configuring the references (foreign keys) in resource migrations. + + This section is only relevant if you are using the migration generator with this resource. + Otherwise, it has no effect. + """, + examples: [ + """ + references do + reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey" + end + """ + ], + entities: [@reference], + schema: [ + polymorphic_on_delete: [ + type: {:one_of, [:delete, :nilify, :nothing, :restrict]}, + doc: + "For polymorphic resources, configures the on_delete behavior of the automatically generated foreign keys to source tables." + ], + polymorphic_on_update: [ + type: {:one_of, [:update, :nilify, :nothing, :restrict]}, + doc: + "For polymorphic resources, configures the on_update behavior of the automatically generated foreign keys to source tables." + ], + polymorphic_name: [ + type: {:one_of, [:update, :nilify, :nothing, :restrict]}, + doc: + "For polymorphic resources, configures the on_update behavior of the automatically generated foreign keys to source tables." + ] + ] + } + + @sqlite %Spark.Dsl.Section{ + name: :sqlite, + describe: """ + Sqlite data layer configuration + """, + sections: [ + @custom_indexes, + @custom_statements, + @references + ], + modules: [ + :repo + ], + examples: [ + """ + sqlite do + repo MyApp.Repo + table "organizations" + end + """ + ], + schema: [ + repo: [ + type: :atom, + required: true, + doc: + "The repo that will be used to fetch your data. See the `AshSqlite.Repo` documentation for more" + ], + migrate?: [ + type: :boolean, + default: true, + doc: + "Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations`" + ], + migration_types: [ + type: :keyword_list, + default: [], + doc: + "A keyword list of attribute names to the ecto migration type that should be used for that attribute. Only necessary if you need to override the defaults." + ], + migration_defaults: [ + type: :keyword_list, + default: [], + doc: """ + A keyword list of attribute names to the ecto migration default that should be used for that attribute. The string you use will be placed verbatim in the migration. Use fragments like `fragment(\\\\"now()\\\\")`, or for `nil`, use `\\\\"nil\\\\"`. + """ + ], + base_filter_sql: [ + type: :string, + doc: + "A raw sql version of the base_filter, e.g `representative = true`. Required if trying to create a unique constraint on a resource with a base_filter" + ], + skip_unique_indexes: [ + type: {:wrap_list, :atom}, + default: false, + doc: "Skip generating unique indexes when generating migrations" + ], + unique_index_names: [ + type: + {:list, + {:or, + [{:tuple, [{:list, :atom}, :string]}, {:tuple, [{:list, :atom}, :string, :string]}]}}, + default: [], + doc: """ + A list of unique index names that could raise errors that are not configured in identities, or an mfa to a function that takes a changeset and returns the list. In the format `{[:affected, :keys], "name_of_constraint"}` or `{[:affected, :keys], "name_of_constraint", "custom error message"}` + """ + ], + exclusion_constraint_names: [ + type: :any, + default: [], + doc: """ + A list of exclusion constraint names that could raise errors. Must be in the format `{:affected_key, "name_of_constraint"}` or `{:affected_key, "name_of_constraint", "custom error message"}` + """ + ], + identity_index_names: [ + type: :any, + default: [], + doc: """ + A keyword list of identity names to the unique index name that they should use when being managed by the migration generator. + """ + ], + foreign_key_names: [ + type: {:list, {:or, [{:tuple, [:atom, :string]}, {:tuple, [:string, :string]}]}}, + default: [], + doc: """ + A list of foreign keys that could raise errors, or an mfa to a function that takes a changeset and returns a list. In the format: `{:key, "name_of_constraint"}` or `{:key, "name_of_constraint", "custom error message"}` + """ + ], + migration_ignore_attributes: [ + type: {:list, :atom}, + default: [], + doc: """ + A list of attributes that will be ignored when generating migrations. + """ + ], + table: [ + type: :string, + doc: """ + The table to store and read the resource from. If this is changed, the migration generator will not remove the old table. + """ + ], + polymorphic?: [ + type: :boolean, + default: false, + doc: """ + Declares this resource as polymorphic. See the [polymorphic resources guide](/documentation/topics/resources/polymorphic-resources.md) for more. + """ + ] + ] + } + + @behaviour Ash.DataLayer + + @sections [@sqlite] + + @moduledoc """ + A sqlite data layer that leverages Ecto's sqlite capabilities. + """ + + use Spark.Dsl.Extension, + sections: @sections, + transformers: [ + AshSqlite.Transformers.ValidateReferences, + AshSqlite.Transformers.VerifyRepo, + AshSqlite.Transformers.EnsureTableOrPolymorphic + ] + + def migrate(args) do + # TODO: take args that we care about + Mix.Task.run("ash_sqlite.migrate", args) + end + + def rollback(args) do + repos = AshSqlite.Mix.Helpers.repos!([], args) + + show_for_repo? = Enum.count_until(repos, 2) == 2 + + for repo <- repos do + for_repo = + if show_for_repo? do + " for repo #{inspect(repo)}" + else + "" + end + + migrations_path = AshSqlite.Mix.Helpers.migrations_path([], repo) + + files = + migrations_path + |> Path.join("**/*.exs") + |> Path.wildcard() + |> Enum.sort() + |> Enum.reverse() + |> Enum.take(20) + |> Enum.map(&String.trim_leading(&1, migrations_path)) + |> Enum.with_index() + |> Enum.map(fn {file, index} -> "#{index + 1}: #{file}" end) + + n = + Mix.shell().prompt( + """ + How many migrations should be rolled back#{for_repo}? (default: 0) + + Last 20 migration names, with the input you must provide to + rollback up to *and including* that migration: + + #{Enum.join(files, "\n")} + Rollback to: + """ + |> String.trim_trailing() + ) + |> String.trim() + |> case do + "" -> + 0 + + n -> + try do + String.to_integer(n) + rescue + _ -> + # credo:disable-for-next-line + raise "Required an integer value, got: #{n}" + end + end + + Mix.Task.run("ash_postgres.rollback", args ++ ["-r", inspect(repo), "-n", to_string(n)]) + Mix.Task.reenable("ash_postgres.rollback") + end + end + + def codegen(args) do + # TODO: take args that we care about + Mix.Task.run("ash_sqlite.generate_migrations", args) + end + + def setup(args) do + # TODO: take args that we care about + Mix.Task.run("ash_sqlite.create", args) + Mix.Task.run("ash_sqlite.migrate", args) + end + + def tear_down(args) do + # TODO: take args that we care about + Mix.Task.run("ash_sqlite.drop", args) + end + + import Ecto.Query, only: [from: 2, subquery: 1] + + @impl true + def can?(_, :async_engine), do: false + def can?(_, :bulk_create), do: true + def can?(_, {:lock, _}), do: false + + def can?(_, :transact), do: false + def can?(_, :composite_primary_key), do: true + def can?(_, {:atomic, :update}), do: true + def can?(_, {:atomic, :upsert}), do: true + def can?(_, :upsert), do: true + def can?(_, :changeset_filter), do: true + + def can?(resource, {:join, other_resource}) do + data_layer = Ash.DataLayer.data_layer(resource) + other_data_layer = Ash.DataLayer.data_layer(other_resource) + + data_layer == other_data_layer and + AshSqlite.DataLayer.Info.repo(resource) == AshSqlite.DataLayer.Info.repo(other_resource) + end + + def can?(_resource, {:lateral_join, _}) do + false + end + + def can?(_, :boolean_filter), do: true + + def can?(_, {:aggregate, _type}), do: false + + def can?(_, :aggregate_filter), do: false + def can?(_, :aggregate_sort), do: false + def can?(_, :expression_calculation), do: true + def can?(_, :expression_calculation_sort), do: true + def can?(_, :create), do: true + def can?(_, :select), do: true + def can?(_, :read), do: true + + def can?(resource, action) when action in ~w[update destroy]a do + resource + |> Ash.Resource.Info.primary_key() + |> Enum.any?() + end + + def can?(_, :filter), do: true + def can?(_, :limit), do: true + def can?(_, :offset), do: true + def can?(_, :multitenancy), do: false + + def can?(_, {:filter_relationship, %{manual: {module, _}}}) do + Spark.implements_behaviour?(module, AshSqlite.ManualRelationship) + end + + def can?(_, {:filter_relationship, _}), do: true + + def can?(_, {:aggregate_relationship, _}), do: false + + def can?(_, :timeout), do: true + def can?(_, {:filter_expr, %Ash.Query.Function.StringJoin{}}), do: false + def can?(_, {:filter_expr, _}), do: true + def can?(_, :nested_expressions), do: true + def can?(_, {:query_aggregate, _}), do: true + def can?(_, :sort), do: true + def can?(_, :distinct_sort), do: false + def can?(_, :distinct), do: false + def can?(_, {:sort, _}), do: true + def can?(_, _), do: false + + @impl true + def limit(query, nil, _), do: {:ok, query} + + def limit(query, limit, _resource) do + {:ok, from(row in query, limit: ^limit)} + end + + @impl true + def source(resource) do + AshSqlite.DataLayer.Info.table(resource) || "" + end + + @impl true + def set_context(resource, data_layer_query, context) do + start_bindings = context[:data_layer][:start_bindings_at] || 0 + data_layer_query = from(row in data_layer_query, as: ^start_bindings) + + data_layer_query = + if context[:data_layer][:table] do + %{ + data_layer_query + | from: %{data_layer_query.from | source: {context[:data_layer][:table], resource}} + } + else + data_layer_query + end + + {:ok, + AshSql.Bindings.default_bindings( + data_layer_query, + resource, + AshSqlite.SqlImplementation, + context + )} + end + + @impl true + def offset(query, nil, _), do: query + + def offset(%{offset: old_offset} = query, 0, _resource) when old_offset in [0, nil] do + {:ok, query} + end + + def offset(query, offset, _resource) do + {:ok, from(row in query, offset: ^offset)} + end + + @impl true + def run_aggregate_query(query, aggregates, resource) do + {exists, aggregates} = Enum.split_with(aggregates, &(&1.kind == :exists)) + query = AshSql.Bindings.default_bindings(query, resource, AshSqlite.SqlImplementation) + + query = + if query.limit do + query = + query + |> Ecto.Query.exclude(:select) + |> Ecto.Query.exclude(:order_by) + |> Map.put(:windows, []) + + from(row in subquery(query), as: ^0, select: %{}) + else + query + |> Ecto.Query.exclude(:select) + |> Ecto.Query.exclude(:order_by) + |> Map.put(:windows, []) + |> Ecto.Query.select(%{}) + end + + query_before_select = query + + query = + Enum.reduce( + aggregates, + query, + fn agg, query -> + AshSql.Aggregate.add_subquery_aggregate_select( + query, + agg.relationship_path |> Enum.drop(1), + agg, + resource, + true, + Ash.Resource.Info.relationship(resource, agg.relationship_path |> Enum.at(1)) + ) + end + ) + + result = + case aggregates do + [] -> + %{} + + _ -> + dynamic_repo(resource, query).one(query, repo_opts(nil, nil, resource)) + end + + {:ok, add_exists_aggs(result, resource, query_before_select, exists)} + end + + defp add_exists_aggs(result, resource, query, exists) do + repo = dynamic_repo(resource, query) + repo_opts = repo_opts(nil, nil, resource) + + Enum.reduce(exists, result, fn agg, result -> + {:ok, filtered} = + case agg do + %{query: %{filter: filter}} when not is_nil(filter) -> + filter(query, filter, resource) + + _ -> + {:ok, query} + end + + Map.put( + result || %{}, + agg.name, + repo.exists?(filtered, repo_opts) + ) + end) + end + + @impl true + def run_query(query, resource) do + with_sort_applied = + if query.__ash_bindings__[:sort_applied?] do + {:ok, query} + else + AshSql.Sort.apply_sort(query, query.__ash_bindings__[:sort], resource) + end + + case with_sort_applied do + {:error, error} -> + {:error, error} + + {:ok, query} -> + query = + if query.__ash_bindings__[:__order__?] && query.windows[:order] do + order_by = %{query.windows[:order] | expr: query.windows[:order].expr[:order_by]} + + %{ + query + | windows: Keyword.delete(query.windows, :order), + order_bys: [order_by] + } + else + %{query | windows: Keyword.delete(query.windows, :order)} + end + + if AshSqlite.DataLayer.Info.polymorphic?(resource) && no_table?(query) do + raise_table_error!(resource, :read) + else + primary_key = Ash.Resource.Info.primary_key(resource) + + {:ok, + dynamic_repo(resource, query).all(query, repo_opts(nil, nil, resource)) + |> Enum.uniq_by(&Map.take(&1, primary_key))} + end + end + rescue + e -> + handle_raised_error(e, __STACKTRACE__, query, resource) + end + + defp no_table?(%{from: %{source: {"", _}}}), do: true + defp no_table?(_), do: false + + defp repo_opts(timeout, nil, _resource) do + [] + |> add_timeout(timeout) + end + + defp repo_opts(timeout, _resource) do + add_timeout([], timeout) + end + + defp add_timeout(opts, timeout) when not is_nil(timeout) do + Keyword.put(opts, :timeout, timeout) + end + + defp add_timeout(opts, _), do: opts + + @impl true + def functions(_resource) do + [ + AshSqlite.Functions.Like, + AshSqlite.Functions.ILike + ] + end + + @impl true + def resource_to_query(resource, _) do + from(row in {AshSqlite.DataLayer.Info.table(resource) || "", resource}, []) + end + + @impl true + def bulk_create(resource, stream, options) do + changesets = Enum.to_list(stream) + + repo = dynamic_repo(resource, Enum.at(changesets, 0)) + + opts = repo_opts(nil, options[:tenant], resource) + + source = resolve_source(resource, Enum.at(changesets, 0)) + + try do + opts = + if options[:upsert?] do + raise "MySQL datalayer doesn't (yet?) know to upsert in bulk_create" + ## Ash groups changesets by atomics before dispatching them to the data layer + ## this means that all changesets have the same atomics + # %{atomics: atomics, filter: filter} = Enum.at(changesets, 0) + + # query = from(row in resource, as: ^0) + + # query = + # query + # |> AshSql.Bindings.default_bindings(resource, AshSqlite.SqlImplementation) + + # upsert_set = + # upsert_set(resource, changesets, options) + + # on_conflict = + # case AshSql.Atomics.query_with_atomics( + # resource, + # query, + # filter, + # atomics, + # %{}, + # upsert_set + # ) do + # :empty -> + # :nothing + + # {:ok, query} -> + # query + + # {:error, error} -> + # raise Ash.Error.to_ash_error(error) + # end + + # opts + # |> Keyword.put(:on_conflict, on_conflict) + # |> Keyword.put( + # :conflict_target, + # conflict_target( + # resource, + # options[:upsert_keys] || Ash.Resource.Info.primary_key(resource) + # ) + # ) + else + opts + end + + ecto_changesets = Enum.map(changesets, & &1.attributes) + + opts = + if schema = Enum.at(changesets, 0).context[:data_layer][:schema] do + Keyword.put(opts, :prefix, schema) + else + opts + end + + resource_for_returning = if options.return_records?, do: resource, else: nil + + result = insert_all_returning(source, ecto_changesets, repo, resource_for_returning, opts) + + case result do + {_, nil} -> + :ok + + {_, results} -> + if options[:single?] do + {:ok, results} + else + {:ok, + Stream.zip_with(results, changesets, fn result, changeset -> + Ash.Resource.put_metadata( + result, + :bulk_create_index, + changeset.context.bulk_create.index + ) + end)} + end + end + rescue + e -> + changeset = Ash.Changeset.new(resource) + + handle_raised_error( + e, + __STACKTRACE__, + {:bulk_create, ecto_changeset(changeset.data, changeset, :create, false)}, + resource + ) + end + end + + defp insert_all_returning(source, entries, repo, nil, opts) do + repo.insert_all(source, entries, opts) + end + + defp insert_all_returning(source, entries, repo, resource, opts) do + {count, nil} = repo.insert_all(source, entries, opts) + reload_key = Ash.Resource.Info.primary_key(resource) |> Enum.at(0) + keys_to_reload = entries |> Enum.map(&Map.get(&1, reload_key)) + + unordered = Ecto.Query.from(s in source, where: s.id in ^keys_to_reload) |> repo.all() + indexed = unordered |> Enum.group_by(&Map.get(&1, reload_key)) + + ordered = + keys_to_reload + |> Enum.map(&(Map.get(indexed, &1) |> Enum.at(0))) + + {count, ordered} + end + + defp upsert_set(resource, changesets, options) do + attributes_changing_anywhere = + changesets |> Enum.flat_map(&Map.keys(&1.attributes)) |> Enum.uniq() + + update_defaults = update_defaults(resource) + # We can't reference EXCLUDED if at least one of the changesets in the stream is not + # changing the value (and we wouldn't want to even if we could as it would be unnecessary) + + upsert_fields = + (options[:upsert_fields] || []) |> Enum.filter(&(&1 in attributes_changing_anywhere)) + + fields_to_upsert = + (upsert_fields ++ Keyword.keys(update_defaults)) -- + Keyword.keys(Enum.at(changesets, 0).atomics) + + Enum.map(fields_to_upsert, fn upsert_field -> + # for safety, we check once more at the end that all values in + # upsert_fields are names of attributes. This is because + # below we use `literal/1` to bring them into the query + if is_nil(resource.__schema__(:type, upsert_field)) do + raise "Only attribute names can be used in upsert_fields" + end + + case Keyword.fetch(update_defaults, upsert_field) do + {:ok, default} -> + if upsert_field in upsert_fields do + {upsert_field, + Ecto.Query.dynamic( + [], + fragment( + "COALESCE(?, ?)", + literal(^to_string(upsert_field)), + ^default + ) + )} + else + {upsert_field, default} + end + + :error -> + {upsert_field, + Ecto.Query.dynamic( + [], + fragment("?", literal(^to_string(upsert_field))) + )} + end + end) + end + + @impl true + def create(resource, changeset) do + changeset = %{ + changeset + | data: + Map.update!( + changeset.data, + :__meta__, + &Map.put(&1, :source, table(resource, changeset)) + ) + } + + case bulk_create(resource, [changeset], %{ + single?: true, + tenant: changeset.tenant, + return_records?: true + }) do + {:ok, [result]} -> + {:ok, result} + + {:error, error} -> + {:error, error} + end + end + + defp handle_errors({:error, %Ecto.Changeset{errors: errors}}) do + {:error, Enum.map(errors, &to_ash_error/1)} + end + + defp to_ash_error({field, {message, vars}}) do + Ash.Error.Changes.InvalidAttribute.exception( + field: field, + message: message, + private_vars: vars + ) + end + + defp ecto_changeset(record, changeset, type, table_error? \\ true) do + filters = + if changeset.action_type == :create do + %{} + else + Map.get(changeset, :filters, %{}) + end + + filters = + if changeset.action_type == :create do + filters + else + changeset.resource + |> Ash.Resource.Info.primary_key() + |> Enum.reduce(filters, fn key, filters -> + Map.put(filters, key, Map.get(record, key)) + end) + end + + attributes = + changeset.resource + |> Ash.Resource.Info.attributes() + |> Enum.map(& &1.name) + + attributes_to_change = + Enum.reject(attributes, fn attribute -> + Keyword.has_key?(changeset.atomics, attribute) + end) + + ecto_changeset = + record + |> to_ecto() + |> set_table(changeset, type, table_error?) + |> Ecto.Changeset.change(Map.take(changeset.attributes, attributes_to_change)) + |> Map.update!(:filters, &Map.merge(&1, filters)) + |> add_configured_foreign_key_constraints(record.__struct__) + |> add_unique_indexes(record.__struct__, changeset) + |> add_exclusion_constraints(record.__struct__) + + case type do + :create -> + ecto_changeset + |> add_my_foreign_key_constraints(record.__struct__) + + type when type in [:upsert, :update] -> + ecto_changeset + |> add_my_foreign_key_constraints(record.__struct__) + |> add_related_foreign_key_constraints(record.__struct__) + + :delete -> + ecto_changeset + |> add_related_foreign_key_constraints(record.__struct__) + end + end + + defp handle_raised_error( + %Ecto.StaleEntryError{changeset: %{data: %resource{}, filters: filters}}, + stacktrace, + context, + resource + ) do + handle_raised_error( + Ash.Error.Changes.StaleRecord.exception(resource: resource, filters: filters), + stacktrace, + context, + resource + ) + end + + defp handle_raised_error(%Ecto.Query.CastError{} = e, stacktrace, context, resource) do + handle_raised_error( + Ash.Error.Query.InvalidFilterValue.exception(value: e.value, context: context), + stacktrace, + context, + resource + ) + end + + defp handle_raised_error( + %MyXQL.Error{ + mysql: %{ + code: 1452, + name: :ER_NO_REFERENCED_ROW_2 + } + }, + stacktrace, + context, + resource + ) do + handle_raised_error( + Ash.Error.Changes.InvalidChanges.exception( + fields: Ash.Resource.Info.primary_key(resource), + message: "referenced something that does not exist" + ), + stacktrace, + context, + resource + ) + end + + defp handle_raised_error( + %MyXQL.Error{ + mysql: %{code: 1062, name: :ER_DUP_ENTRY} + }, + _stacktrace, + _context, + resource + ) do + fields="" + names = + fields + |> String.split(", ") + |> Enum.map(fn field -> + field |> String.split(".", trim: true) |> Enum.drop(1) |> Enum.at(0) + end) + |> Enum.map(fn field -> + Ash.Resource.Info.attribute(resource, field) + end) + |> Enum.reject(&is_nil/1) + #|> Enum.map(fn %{name: name} -> + # name + #end) + + names=[:id] + message = find_constraint_message(resource, names) + + {:error, + names + |> Enum.map(fn name -> + Ash.Error.Changes.InvalidAttribute.exception( + field: name, + message: message + ) + end)} + end + + defp handle_raised_error(error, stacktrace, _ecto_changeset, _resource) do + {:error, Ash.Error.to_ash_error(error, stacktrace)} + end + + defp find_constraint_message(resource, names) do + find_custom_index_message(resource, names) || find_identity_message(resource, names) || + "has already been taken" + end + + defp find_custom_index_message(resource, names) do + resource + |> AshSqlite.DataLayer.Info.custom_indexes() + |> Enum.find(fn %{fields: fields} -> + fields |> Enum.map(&to_string/1) |> Enum.sort() == + names |> Enum.map(&to_string/1) |> Enum.sort() + end) + |> case do + %{message: message} when is_binary(message) -> message + _ -> nil + end + end + + defp find_identity_message(resource, names) do + resource + |> Ash.Resource.Info.identities() + |> Enum.find(fn %{keys: fields} -> + fields |> Enum.map(&to_string/1) |> Enum.sort() == + names |> Enum.map(&to_string/1) |> Enum.sort() + end) + |> case do + %{message: message} when is_binary(message) -> + message + + _ -> + nil + end + end + + defp set_table(record, changeset, operation, table_error?) do + if AshSqlite.DataLayer.Info.polymorphic?(record.__struct__) do + table = + changeset.context[:data_layer][:table] || + AshSqlite.DataLayer.Info.table(record.__struct__) + + if table do + Ecto.put_meta(record, source: table) + else + if table_error? do + raise_table_error!(changeset.resource, operation) + else + record + end + end + else + record + end + end + + def from_ecto({:ok, result}), do: {:ok, from_ecto(result)} + def from_ecto({:error, _} = other), do: other + + def from_ecto(nil), do: nil + + def from_ecto(value) when is_list(value) do + Enum.map(value, &from_ecto/1) + end + + def from_ecto(%resource{} = record) do + if Spark.Dsl.is?(resource, Ash.Resource) do + empty = struct(resource) + + resource + |> Ash.Resource.Info.relationships() + |> Enum.reduce(record, fn relationship, record -> + case Map.get(record, relationship.name) do + %Ecto.Association.NotLoaded{} -> + Map.put(record, relationship.name, Map.get(empty, relationship.name)) + + value -> + Map.put(record, relationship.name, from_ecto(value)) + end + end) + else + record + end + end + + def from_ecto(other), do: other + + def to_ecto(nil), do: nil + + def to_ecto(value) when is_list(value) do + Enum.map(value, &to_ecto/1) + end + + def to_ecto(%resource{} = record) do + if Spark.Dsl.is?(resource, Ash.Resource) do + resource + |> Ash.Resource.Info.relationships() + |> Enum.reduce(record, fn relationship, record -> + value = + case Map.get(record, relationship.name) do + %Ash.NotLoaded{} -> + %Ecto.Association.NotLoaded{ + __field__: relationship.name, + __cardinality__: relationship.cardinality + } + + value -> + to_ecto(value) + end + + Map.put(record, relationship.name, value) + end) + else + record + end + end + + def to_ecto(other), do: other + + defp add_exclusion_constraints(changeset, resource) do + resource + |> AshSqlite.DataLayer.Info.exclusion_constraint_names() + |> Enum.reduce(changeset, fn constraint, changeset -> + case constraint do + {key, name} -> + Ecto.Changeset.exclusion_constraint(changeset, key, name: name) + + {key, name, message} -> + Ecto.Changeset.exclusion_constraint(changeset, key, name: name, message: message) + end + end) + end + + defp add_related_foreign_key_constraints(changeset, resource) do + # TODO: this doesn't guarantee us to get all of them, because if something is related to this + # schema and there is no back-relation, then this won't catch it's foreign key constraints + resource + |> Ash.Resource.Info.relationships() + |> Enum.map(& &1.destination) + |> Enum.uniq() + |> Enum.flat_map(fn related -> + related + |> Ash.Resource.Info.relationships() + |> Enum.filter(&(&1.destination == resource)) + |> Enum.map(&Map.take(&1, [:source, :source_attribute, :destination_attribute, :name])) + end) + |> Enum.reduce(changeset, fn %{ + source: source, + source_attribute: source_attribute, + destination_attribute: destination_attribute, + name: relationship_name + }, + changeset -> + case AshSqlite.DataLayer.Info.reference(resource, relationship_name) do + %{name: name} when not is_nil(name) -> + Ecto.Changeset.foreign_key_constraint(changeset, destination_attribute, + name: name, + message: "would leave records behind" + ) + + _ -> + Ecto.Changeset.foreign_key_constraint(changeset, destination_attribute, + name: "#{AshSqlite.DataLayer.Info.table(source)}_#{source_attribute}_fkey", + message: "would leave records behind" + ) + end + end) + end + + defp add_my_foreign_key_constraints(changeset, resource) do + resource + |> Ash.Resource.Info.relationships() + |> Enum.reduce(changeset, &Ecto.Changeset.foreign_key_constraint(&2, &1.source_attribute)) + end + + defp add_configured_foreign_key_constraints(changeset, resource) do + resource + |> AshSqlite.DataLayer.Info.foreign_key_names() + |> case do + {m, f, a} -> List.wrap(apply(m, f, [changeset | a])) + value -> List.wrap(value) + end + |> Enum.reduce(changeset, fn + {key, name}, changeset -> + Ecto.Changeset.foreign_key_constraint(changeset, key, name: name) + + {key, name, message}, changeset -> + Ecto.Changeset.foreign_key_constraint(changeset, key, name: name, message: message) + end) + end + + defp add_unique_indexes(changeset, resource, ash_changeset) do + changeset = + resource + |> Ash.Resource.Info.identities() + |> Enum.reduce(changeset, fn identity, changeset -> + name = + AshSqlite.DataLayer.Info.identity_index_names(resource)[identity.name] || + "#{table(resource, ash_changeset)}_#{identity.name}_index" + + opts = + if Map.get(identity, :message) do + [name: name, message: identity.message] + else + [name: name] + end + + Ecto.Changeset.unique_constraint(changeset, identity.keys, opts) + end) + + changeset = + resource + |> AshSqlite.DataLayer.Info.custom_indexes() + |> Enum.reduce(changeset, fn index, changeset -> + opts = + if index.message do + [name: index.name, message: index.message] + else + [name: index.name] + end + + Ecto.Changeset.unique_constraint(changeset, index.fields, opts) + end) + + names = + resource + |> AshSqlite.DataLayer.Info.unique_index_names() + |> case do + {m, f, a} -> List.wrap(apply(m, f, [changeset | a])) + value -> List.wrap(value) + end + + names = + case Ash.Resource.Info.primary_key(resource) do + [] -> + names + + fields -> + if table = table(resource, ash_changeset) do + [{fields, table <> "_pkey"} | names] + else + [] + end + end + + Enum.reduce(names, changeset, fn + {keys, name}, changeset -> + Ecto.Changeset.unique_constraint(changeset, List.wrap(keys), name: name) + + {keys, name, message}, changeset -> + Ecto.Changeset.unique_constraint(changeset, List.wrap(keys), name: name, message: message) + end) + end + + @impl true + def upsert(resource, changeset, keys \\ nil) do + keys = keys || Ash.Resource.Info.primary_key(keys) + + explicitly_changing_attributes = + Map.keys(changeset.attributes) -- Map.get(changeset, :defaults, []) -- keys + + upsert_fields = + changeset.context[:private][:upsert_fields] || explicitly_changing_attributes + + case bulk_create(resource, [changeset], %{ + single?: true, + upsert?: true, + tenant: changeset.tenant, + upsert_keys: keys, + upsert_fields: upsert_fields, + return_records?: true + }) do + {:ok, [result]} -> + {:ok, result} + + {:error, error} -> + {:error, error} + end + end + + defp conflict_target(resource, keys) do + if Ash.Resource.Info.base_filter(resource) do + base_filter_sql = + AshSqlite.DataLayer.Info.base_filter_sql(resource) || + raise """ + Cannot use upserts with resources that have a base_filter without also adding `base_filter_sql` in the sqlite section. + """ + + sources = + Enum.map(keys, fn key -> + ~s("#{Ash.Resource.Info.attribute(resource, key).source || key}") + end) + + {:unsafe_fragment, "(" <> Enum.join(sources, ", ") <> ") WHERE (#{base_filter_sql})"} + else + keys + end + end + + defp update_defaults(resource) do + attributes = + resource + |> Ash.Resource.Info.attributes() + |> Enum.reject(&is_nil(&1.update_default)) + + attributes + |> static_defaults() + |> Enum.concat(lazy_matching_defaults(attributes)) + |> Enum.concat(lazy_non_matching_defaults(attributes)) + end + + defp static_defaults(attributes) do + attributes + |> Enum.reject(&get_default_fun(&1)) + |> Enum.map(&{&1.name, &1.update_default}) + end + + defp lazy_non_matching_defaults(attributes) do + attributes + |> Enum.filter(&(!&1.match_other_defaults? && get_default_fun(&1))) + |> Enum.map(fn attribute -> + default_value = + case attribute.update_default do + function when is_function(function) -> + function.() + + {m, f, a} when is_atom(m) and is_atom(f) and is_list(a) -> + apply(m, f, a) + end + + {attribute.name, default_value} + end) + end + + defp lazy_matching_defaults(attributes) do + attributes + |> Enum.filter(&(&1.match_other_defaults? && get_default_fun(&1))) + |> Enum.group_by(& &1.update_default) + |> Enum.flat_map(fn {default_fun, attributes} -> + default_value = + case default_fun do + function when is_function(function) -> + function.() + + {m, f, a} when is_atom(m) and is_atom(f) and is_list(a) -> + apply(m, f, a) + end + + Enum.map(attributes, &{&1.name, default_value}) + end) + end + + defp get_default_fun(attribute) do + if is_function(attribute.update_default) or match?({_, _, _}, attribute.update_default) do + attribute.update_default + end + end + + @impl true + def update(resource, changeset) do + ecto_changeset = + changeset.data + |> Map.update!(:__meta__, &Map.put(&1, :source, table(resource, changeset))) + |> ecto_changeset(changeset, :update) + + try do + query = from(row in resource, as: ^0) + + select = Keyword.keys(changeset.atomics) ++ Ash.Resource.Info.primary_key(resource) + + query = + query + |> AshSql.Bindings.default_bindings( + resource, + AshSqlite.SqlImplementation, + changeset.context + ) + #|> Ecto.Query.select(^select) + |> pkey_filter(changeset.data) + + case AshSql.Atomics.query_with_atomics( + resource, + query, + changeset.filter, + changeset.atomics, + ecto_changeset.changes, + [] + ) do + :empty -> + {:ok, changeset.data} + + {:ok, query} -> + repo_opts = repo_opts(changeset.timeout, changeset.tenant, changeset.resource) + + #repo_opts = + # Keyword.put(repo_opts, :returning, Keyword.keys(changeset.atomics)) + + repo = dynamic_repo(resource, changeset) + {count, nil} = + repo.update_all(query, [], repo_opts) + + result = from(row in resource, as: ^0, select: ^select) + |> pkey_filter(changeset.data) + |> repo.all() + + case {count, result} do + {0, []} -> + {:error, + Ash.Error.Changes.StaleRecord.exception( + resource: resource, + filters: changeset.filter + )} + + {1, [result]} -> + record = + changeset.data + |> Map.merge(changeset.attributes) + |> Map.merge(Map.take(result, Keyword.keys(changeset.atomics))) + + {:ok, record} + end + + {:error, error} -> + {:error, error} + end + rescue + e -> + handle_raised_error(e, __STACKTRACE__, ecto_changeset, resource) + end + end + + defp pkey_filter(query, %resource{} = record) do + pkey = + record + |> Map.take(Ash.Resource.Info.primary_key(resource)) + |> Map.to_list() + + Ecto.Query.where(query, ^pkey) + end + + @impl true + def destroy(resource, %{data: record} = changeset) do + ecto_changeset = ecto_changeset(record, changeset, :delete) + + try do + ecto_changeset + |> dynamic_repo(resource, changeset).delete( + repo_opts(changeset.timeout, changeset.resource) + ) + |> from_ecto() + |> case do + {:ok, _record} -> + :ok + + {:error, error} -> + handle_errors({:error, error}) + end + rescue + e -> + handle_raised_error(e, __STACKTRACE__, ecto_changeset, resource) + end + end + + @impl true + def sort(query, sort, _resource) do + {:ok, Map.update!(query, :__ash_bindings__, &Map.put(&1, :sort, sort))} + end + + @impl true + def select(query, select, resource) do + query = AshSql.Bindings.default_bindings(query, resource, AshSqlite.SqlImplementation) + + {:ok, + from(row in query, + select: struct(row, ^Enum.uniq(select)) + )} + end + + @doc false + def unwrap_one([thing]), do: thing + def unwrap_one([]), do: nil + def unwrap_one(other), do: other + + @impl true + def filter(query, filter, _resource, opts \\ []) do + query + |> AshSql.Join.join_all_relationships(filter, opts) + |> case do + {:ok, query} -> + {:ok, AshSql.Filter.add_filter_expression(query, filter)} + + {:error, error} -> + {:error, error} + end + end + + @impl true + def add_calculations(query, calculations, resource) do + AshSql.Calculation.add_calculations(query, calculations, resource, 0, true) + end + + @doc false + def get_binding(resource, path, query, type, name_match \\ nil) + + def get_binding(resource, path, %{__ash_bindings__: _} = query, type, name_match) do + types = List.wrap(type) + + Enum.find_value(query.__ash_bindings__.bindings, fn + {binding, %{path: candidate_path, type: binding_type} = data} -> + if binding_type in types do + if name_match do + if data[:name] == name_match do + if Ash.SatSolver.synonymous_relationship_paths?(resource, candidate_path, path) do + binding + end + end + else + if Ash.SatSolver.synonymous_relationship_paths?(resource, candidate_path, path) do + binding + else + false + end + end + end + + _ -> + nil + end) + end + + def get_binding(_, _, _, _, _), do: nil + + @doc false + def add_binding(query, data, additional_bindings \\ 0) do + current = query.__ash_bindings__.current + bindings = query.__ash_bindings__.bindings + + new_ash_bindings = %{ + query.__ash_bindings__ + | bindings: Map.put(bindings, current, data), + current: current + 1 + additional_bindings + } + + %{query | __ash_bindings__: new_ash_bindings} + end + + def add_known_binding(query, data, known_binding) do + bindings = query.__ash_bindings__.bindings + + new_ash_bindings = %{ + query.__ash_bindings__ + | bindings: Map.put(bindings, known_binding, data) + } + + %{query | __ash_bindings__: new_ash_bindings} + end + + @impl true + def rollback(resource, term) do + AshSqlite.DataLayer.Info.repo(resource).rollback(term) + end + + defp table(resource, changeset) do + changeset.context[:data_layer][:table] || AshSqlite.DataLayer.Info.table(resource) + end + + defp raise_table_error!(resource, operation) do + if AshSqlite.DataLayer.Info.polymorphic?(resource) do + raise """ + Could not determine table for #{operation} on #{inspect(resource)}. + + Polymorphic resources require that the `data_layer[:table]` context is provided. + See the guide on polymorphic resources for more information. + """ + else + raise """ + Could not determine table for #{operation} on #{inspect(resource)}. + """ + end + end + + defp dynamic_repo(resource, %{__ash_bindings__: %{context: %{data_layer: %{repo: repo}}}}) do + repo || AshSqlite.DataLayer.Info.repo(resource) + end + + defp dynamic_repo(resource, %{context: %{data_layer: %{repo: repo}}}) do + repo || AshSqlite.DataLayer.Info.repo(resource) + end + + defp dynamic_repo(resource, _) do + AshSqlite.DataLayer.Info.repo(resource) + end + + defp resolve_source(resource, changeset) do + if table = changeset.context[:data_layer][:table] do + {table, resource} + else + resource + end + end +end diff --git a/lib/data_layer/info.ex b/lib/data_layer/info.ex new file mode 100644 index 0000000..abb5193 --- /dev/null +++ b/lib/data_layer/info.ex @@ -0,0 +1,117 @@ +defmodule AshSqlite.DataLayer.Info do + @moduledoc "Introspection functions for " + + alias Spark.Dsl.Extension + + @doc "The configured repo for a resource" + def repo(resource) do + Extension.get_opt(resource, [:sqlite], :repo, nil, true) + end + + @doc "The configured table for a resource" + def table(resource) do + Extension.get_opt(resource, [:sqlite], :table, nil, true) + end + + @doc "The configured references for a resource" + def references(resource) do + Extension.get_entities(resource, [:sqlite, :references]) + end + + @doc "The configured reference for a given relationship of a resource" + def reference(resource, relationship) do + resource + |> Extension.get_entities([:sqlite, :references]) + |> Enum.find(&(&1.relationship == relationship)) + end + + @doc "A keyword list of customized migration types" + def migration_types(resource) do + Extension.get_opt(resource, [:sqlite], :migration_types, []) + end + + @doc "A keyword list of customized migration defaults" + def migration_defaults(resource) do + Extension.get_opt(resource, [:sqlite], :migration_defaults, []) + end + + @doc "A list of attributes to be ignored when generating migrations" + def migration_ignore_attributes(resource) do + Extension.get_opt(resource, [:sqlite], :migration_ignore_attributes, []) + end + + @doc "The configured custom_indexes for a resource" + def custom_indexes(resource) do + Extension.get_entities(resource, [:sqlite, :custom_indexes]) + end + + @doc "The configured custom_statements for a resource" + def custom_statements(resource) do + Extension.get_entities(resource, [:sqlite, :custom_statements]) + end + + @doc "The configured polymorphic_reference_on_delete for a resource" + def polymorphic_on_delete(resource) do + Extension.get_opt(resource, [:sqlite, :references], :polymorphic_on_delete, nil, true) + end + + @doc "The configured polymorphic_reference_on_update for a resource" + def polymorphic_on_update(resource) do + Extension.get_opt(resource, [:sqlite, :references], :polymorphic_on_update, nil, true) + end + + @doc "The configured polymorphic_reference_name for a resource" + def polymorphic_name(resource) do + Extension.get_opt(resource, [:sqlite, :references], :polymorphic_on_delete, nil, true) + end + + @doc "The configured polymorphic? for a resource" + def polymorphic?(resource) do + Extension.get_opt(resource, [:sqlite], :polymorphic?, nil, true) + end + + @doc "The configured unique_index_names" + def unique_index_names(resource) do + Extension.get_opt(resource, [:sqlite], :unique_index_names, [], true) + end + + @doc "The configured exclusion_constraint_names" + def exclusion_constraint_names(resource) do + Extension.get_opt(resource, [:sqlite], :exclusion_constraint_names, [], true) + end + + @doc "The configured identity_index_names" + def identity_index_names(resource) do + Extension.get_opt(resource, [:sqlite], :identity_index_names, [], true) + end + + @doc "Identities not to include in the migrations" + def skip_identities(resource) do + Extension.get_opt(resource, [:sqlite], :skip_identities, [], true) + end + + @doc "The configured foreign_key_names" + def foreign_key_names(resource) do + Extension.get_opt(resource, [:sqlite], :foreign_key_names, [], true) + end + + @doc "Whether or not the resource should be included when generating migrations" + def migrate?(resource) do + Extension.get_opt(resource, [:sqlite], :migrate?, nil, true) + end + + @doc "A list of keys to always include in upserts." + def global_upsert_keys(resource) do + Extension.get_opt(resource, [:sqlite], :global_upsert_keys, []) + end + + @doc "A stringified version of the base_filter, to be used in a where clause when generating unique indexes" + def base_filter_sql(resource) do + Extension.get_opt(resource, [:sqlite], :base_filter_sql, nil) + end + + @doc "Skip generating unique indexes when generating migrations" + def skip_unique_indexes(resource) do + Extension.get_opt(resource, [:sqlite], :skip_unique_indexes, []) + end +end diff --git a/lib/functions/ilike.ex b/lib/functions/ilike.ex new file mode 100644 index 0000000..be9896f --- /dev/null +++ b/lib/functions/ilike.ex @@ -0,0 +1,9 @@ +defmodule AshSqlite.Functions.ILike do + @moduledoc """ + Maps to the builtin sqlite function `ilike`. + """ + + use Ash.Query.Function, name: :ilike + + def args, do: [[:string, :string]] +end diff --git a/lib/functions/like.ex b/lib/functions/like.ex new file mode 100644 index 0000000..442b807 --- /dev/null +++ b/lib/functions/like.ex @@ -0,0 +1,9 @@ +defmodule AshSqlite.Functions.Like do + @moduledoc """ + Maps to the builtin sqlite function `like`. + """ + + use Ash.Query.Function, name: :like + + def args, do: [[:string, :string]] +end diff --git a/lib/manual_relationship.ex b/lib/manual_relationship.ex new file mode 100644 index 0000000..8d26497 --- /dev/null +++ b/lib/manual_relationship.ex @@ -0,0 +1,25 @@ +defmodule AshSqlite.ManualRelationship do + @moduledoc "A behavior for sqlite-specific manual relationship functionality" + + @callback ash_sqlite_join( + source_query :: Ecto.Query.t(), + opts :: Keyword.t(), + current_binding :: term, + destination_binding :: term, + type :: :inner | :left, + destination_query :: Ecto.Query.t() + ) :: {:ok, Ecto.Query.t()} | {:error, term} + + @callback ash_sqlite_subquery( + opts :: Keyword.t(), + current_binding :: term, + destination_binding :: term, + destination_query :: Ecto.Query.t() + ) :: {:ok, Ecto.Query.t()} | {:error, term} + + defmacro __using__(_) do + quote do + @behaviour AshSqlite.ManualRelationship + end + end +end diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex new file mode 100644 index 0000000..8179048 --- /dev/null +++ b/lib/migration_generator/migration_generator.ex @@ -0,0 +1,2542 @@ +defmodule AshSqlite.MigrationGenerator do + @moduledoc false + + require Logger + + import Mix.Generator + + alias AshSqlite.MigrationGenerator.{Operation, Phase} + + defstruct snapshot_path: nil, + migration_path: nil, + name: nil, + quiet: false, + current_snapshots: nil, + answers: [], + no_shell?: false, + format: true, + dry_run: false, + check: false, + drop_columns: false + + def generate(domains, opts \\ []) do + domains = List.wrap(domains) + opts = opts(opts) + + all_resources = Enum.uniq(Enum.flat_map(domains, &Ash.Domain.Info.resources/1)) + + snapshots = + all_resources + |> Enum.filter(fn resource -> + Ash.DataLayer.data_layer(resource) == AshSqlite.DataLayer && + AshSqlite.DataLayer.Info.migrate?(resource) + end) + |> Enum.flat_map(&get_snapshots(&1, all_resources)) + + repos = + snapshots + |> Enum.map(& &1.repo) + |> Enum.uniq() + + Mix.shell().info("\nExtension Migrations: ") + create_extension_migrations(repos, opts) + Mix.shell().info("\nGenerating Migrations:") + create_migrations(snapshots, opts) + end + + @doc """ + A work in progress utility for getting snapshots. + + Does not support everything supported by the migration generator. + """ + def take_snapshots(domain, repo, only_resources \\ nil) do + all_resources = domain |> Ash.Domain.Info.resources() |> Enum.uniq() + + all_resources + |> Enum.filter(fn resource -> + Ash.DataLayer.data_layer(resource) == AshSqlite.DataLayer && + AshSqlite.DataLayer.Info.repo(resource) == repo && + (is_nil(only_resources) || resource in only_resources) + end) + |> Enum.flat_map(&get_snapshots(&1, all_resources)) + end + + @doc """ + A work in progress utility for getting operations between snapshots. + + Does not support everything supported by the migration generator. + """ + def get_operations_from_snapshots(old_snapshots, new_snapshots, opts \\ []) do + opts = %{opts(opts) | no_shell?: true} + + old_snapshots = + old_snapshots + |> Enum.map(&sanitize_snapshot/1) + + new_snapshots + |> deduplicate_snapshots(opts, old_snapshots) + |> fetch_operations(opts) + |> Enum.flat_map(&elem(&1, 1)) + |> Enum.uniq() + |> organize_operations() + end + + defp add_references_primary_key(snapshot, snapshots) do + %{ + snapshot + | attributes: + snapshot.attributes + |> Enum.map(fn + %{references: references} = attribute when not is_nil(references) -> + if is_nil(Map.get(references, :primary_key?)) do + %{ + attribute + | references: + Map.put( + references, + :primary_key?, + find_references_primary_key( + references, + snapshots + ) + ) + } + else + attribute + end + + attribute -> + attribute + end) + } + end + + defp find_references_primary_key(references, snapshots) do + Enum.find_value(snapshots, false, fn snapshot -> + if snapshot && references && snapshot.table == references.table do + Enum.any?(snapshot.attributes, fn attribute -> + attribute.source == references.destination_attribute && attribute.primary_key? + end) + end + end) + end + + defp opts(opts) do + case struct(__MODULE__, opts) do + %{check: true} = opts -> + %{opts | dry_run: true} + + opts -> + opts + end + end + + defp snapshot_path(%{snapshot_path: snapshot_path}, _) when not is_nil(snapshot_path), + do: snapshot_path + + defp snapshot_path(_config, repo) do + # Copied from ecto's mix task, thanks Ecto â¤ï¸ + config = repo.config() + + app = Keyword.fetch!(config, :otp_app) + Path.join([Mix.Project.deps_paths()[app] || File.cwd!(), "priv", "resource_snapshots"]) + end + + defp create_extension_migrations(repos, opts) do + for repo <- repos do + snapshot_path = snapshot_path(opts, repo) + snapshot_file = Path.join(snapshot_path, "extensions.json") + + installed_extensions = + if File.exists?(snapshot_file) do + snapshot_file + |> File.read!() + |> Jason.decode!(keys: :atoms!) + else + [] + end + + {_extensions_snapshot, installed_extensions} = + case installed_extensions do + installed when is_list(installed) -> + {%{ + installed: installed + }, installed} + + other -> + {other, other.installed} + end + + requesteds = + repo.installed_extensions() + |> Enum.map(fn + extension_module when is_atom(extension_module) -> + {ext_name, version, _up_fn, _down_fn} = extension = extension_module.extension() + + {"#{ext_name}_v#{version}", extension} + + extension_name -> + {extension_name, extension_name} + end) + + to_install = + requesteds + |> Enum.filter(fn {name, _extension} -> !Enum.member?(installed_extensions, name) end) + |> Enum.map(fn {_name, extension} -> extension end) + + if Enum.empty?(to_install) do + Mix.shell().info("No extensions to install") + :ok + else + {module, migration_name} = + case to_install do + [{ext_name, version, _up_fn, _down_fn}] -> + {"install_#{ext_name}_v#{version}", + "#{timestamp(true)}_install_#{ext_name}_v#{version}_extension"} + + [single] -> + {"install_#{single}", "#{timestamp(true)}_install_#{single}_extension"} + + multiple -> + {"install_#{Enum.count(multiple)}_extensions", + "#{timestamp(true)}_install_#{Enum.count(multiple)}_extensions"} + end + + migration_file = + opts + |> migration_path(repo) + |> Path.join(migration_name <> ".exs") + + sanitized_module = + module + |> String.replace("-", "_") + |> Macro.camelize() + + module_name = Module.concat([repo, Migrations, sanitized_module]) + + install = + Enum.map_join(to_install, "\n", fn + {_ext_name, version, up_fn, _down_fn} when is_function(up_fn, 1) -> + up_fn.(version) + + extension -> + raise "only custom extensions supported currently. Got #{inspect(extension)}" + end) + + uninstall = + Enum.map_join(to_install, "\n", fn + {_ext_name, version, _up_fn, down_fn} when is_function(down_fn, 1) -> + down_fn.(version) + + extension -> + raise "only custom extensions supported currently. Got #{inspect(extension)}" + end) + + contents = """ + defmodule #{inspect(module_name)} do + @moduledoc \"\"\" + Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback + + This file was autogenerated with `mix ash_sqlite.generate_migrations` + \"\"\" + + use Ecto.Migration + + def up do + #{install} + end + + def down do + # Uncomment this if you actually want to uninstall the extensions + # when this migration is rolled back: + #{uninstall} + end + end + """ + + installed = Enum.map(requesteds, fn {name, _extension} -> name end) + + snapshot_contents = + Jason.encode!( + %{ + installed: installed + }, + pretty: true + ) + + contents = format(contents, opts) + create_file(snapshot_file, snapshot_contents, force: true) + create_file(migration_file, contents) + end + end + end + + defp create_migrations(snapshots, opts) do + snapshots + |> Enum.group_by(& &1.repo) + |> Enum.each(fn {repo, snapshots} -> + deduped = deduplicate_snapshots(snapshots, opts) + + snapshots_with_operations = + deduped + |> fetch_operations(opts) + |> Enum.map(&add_order_to_operations/1) + + snapshots = Enum.map(snapshots_with_operations, &elem(&1, 0)) + + snapshots_with_operations + |> Enum.flat_map(&elem(&1, 1)) + |> Enum.uniq() + |> case do + [] -> + Mix.shell().info( + "No changes detected, so no migrations or snapshots have been created." + ) + + :ok + + operations -> + 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 + |> build_up_and_down() + |> write_migration!(repo, opts) + + create_new_snapshot(snapshots, repo_name(repo), opts) + end + end) + end + + defp add_order_to_operations({snapshot, operations}) do + operations_with_order = Enum.map(operations, &add_order_to_operation(&1, snapshot.attributes)) + + {snapshot, operations_with_order} + end + + defp add_order_to_operation(%{attribute: attribute} = op, attributes) do + order = Enum.find_index(attributes, &(&1.source == attribute.source)) + attribute = Map.put(attribute, :order, order) + + %{op | attribute: attribute} + end + + defp add_order_to_operation(%{new_attribute: attribute} = op, attributes) do + order = Enum.find_index(attributes, &(&1.source == attribute.source)) + attribute = Map.put(attribute, :order, order) + + %{op | new_attribute: attribute} + end + + defp add_order_to_operation(op, _), do: op + + defp organize_operations([]), do: [] + + defp organize_operations(operations) do + operations + |> sort_operations() + |> streamline() + |> group_into_phases() + |> clean_phases() + end + + defp clean_phases(phases) do + phases + |> Enum.flat_map(fn + %{operations: []} -> + [] + + %{operations: operations} = phase -> + if Enum.all?(operations, &match?(%{commented?: true}, &1)) do + [%{phase | commented?: true}] + else + [phase] + end + + op -> + [op] + end) + end + + defp deduplicate_snapshots(snapshots, opts, existing_snapshots \\ []) do + grouped = + snapshots + |> Enum.group_by(fn snapshot -> + snapshot.table + end) + + old_snapshots = + Map.new(grouped, fn {key, [snapshot | _]} -> + old_snapshot = + if opts.no_shell? do + Enum.find(existing_snapshots, &(&1.table == snapshot.table)) + else + get_existing_snapshot(snapshot, opts) + end + + { + key, + old_snapshot + } + end) + + old_snapshots_list = Map.values(old_snapshots) + + old_snapshots = + Map.new(old_snapshots, fn {key, old_snapshot} -> + if old_snapshot do + {key, add_references_primary_key(old_snapshot, old_snapshots_list)} + else + {key, old_snapshot} + end + end) + + grouped + |> Enum.map(fn {key, [snapshot | _] = snapshots} -> + existing_snapshot = old_snapshots[key] + + {primary_key, identities} = merge_primary_keys(existing_snapshot, snapshots, opts) + + attributes = Enum.flat_map(snapshots, & &1.attributes) + + count_with_create = Enum.count(snapshots, & &1.has_create_action) + + new_snapshot = %{ + snapshot + | attributes: merge_attributes(attributes, snapshot.table, count_with_create), + identities: snapshots |> Enum.flat_map(& &1.identities) |> Enum.uniq(), + custom_indexes: snapshots |> Enum.flat_map(& &1.custom_indexes) |> Enum.uniq(), + custom_statements: snapshots |> Enum.flat_map(& &1.custom_statements) |> Enum.uniq() + } + + all_identities = + new_snapshot.identities + |> Kernel.++(identities) + |> Enum.sort_by(& &1.name) + # We sort the identities by there being an identity with a matching name in the existing snapshot + # so that we prefer identities that currently exist over new ones + |> Enum.sort_by(fn identity -> + existing_snapshot + |> Kernel.||(%{}) + |> Map.get(:identities, []) + |> Enum.any?(fn existing_identity -> + existing_identity.name == identity.name + end) + |> Kernel.!() + end) + |> Enum.uniq_by(fn identity -> + {Enum.sort(identity.keys), identity.base_filter} + end) + + new_snapshot = %{new_snapshot | identities: all_identities} + + { + %{ + new_snapshot + | attributes: + Enum.map(new_snapshot.attributes, fn attribute -> + if attribute.source in primary_key do + %{attribute | primary_key?: true} + else + %{attribute | primary_key?: false} + end + end) + }, + existing_snapshot + } + end) + end + + defp merge_attributes(attributes, table, count) do + attributes + |> Enum.with_index() + |> Enum.map(fn {attr, i} -> Map.put(attr, :order, i) end) + |> Enum.group_by(& &1.source) + |> Enum.map(fn {source, attributes} -> + size = + attributes + |> Enum.map(& &1.size) + |> Enum.filter(& &1) + |> case do + [] -> + nil + + sizes -> + Enum.max(sizes) + end + + %{ + source: source, + type: merge_types(Enum.map(attributes, & &1.type), source, table), + size: size, + default: merge_defaults(Enum.map(attributes, & &1.default)), + allow_nil?: Enum.any?(attributes, & &1.allow_nil?) || Enum.count(attributes) < count, + generated?: Enum.any?(attributes, & &1.generated?), + references: merge_references(Enum.map(attributes, & &1.references), source, table), + primary_key?: false, + order: attributes |> Enum.map(& &1.order) |> Enum.min() + } + end) + |> Enum.sort(&(&1.order < &2.order)) + |> Enum.map(&Map.drop(&1, [:order])) + end + + defp merge_references(references, name, table) do + references + |> Enum.reject(&is_nil/1) + |> Enum.uniq() + |> case do + [] -> + nil + + references -> + %{ + destination_attribute: merge_uniq!(references, table, :destination_attribute, name), + deferrable: merge_uniq!(references, table, :deferrable, name), + destination_attribute_default: + merge_uniq!(references, table, :destination_attribute_default, name), + destination_attribute_generated: + merge_uniq!(references, table, :destination_attribute_generated, name), + multitenancy: merge_uniq!(references, table, :multitenancy, name), + primary_key?: merge_uniq!(references, table, :primary_key?, name), + on_delete: merge_uniq!(references, table, :on_delete, name), + on_update: merge_uniq!(references, table, :on_update, name), + name: merge_uniq!(references, table, :name, name), + table: merge_uniq!(references, table, :table, name) + } + end + end + + defp merge_uniq!(references, table, field, attribute) do + references + |> Enum.map(&Map.get(&1, field)) + |> Enum.reject(&is_nil/1) + |> Enum.uniq() + |> case do + [] -> + nil + + [value] -> + value + + values -> + values = Enum.map_join(values, "\n", &" * #{inspect(&1)}") + + raise """ + Conflicting configurations for references for #{table}.#{attribute}: + + Values: + + #{values} + """ + end + end + + defp merge_types(types, name, table) do + types + |> Enum.uniq() + |> case do + [type] -> + type + + types -> + raise "Conflicting types for table `#{table}.#{name}`: #{inspect(types)}" + end + end + + defp merge_defaults(defaults) do + defaults + |> Enum.uniq() + |> case do + [default] -> default + _ -> "nil" + end + end + + defp merge_primary_keys(nil, [snapshot | _] = snapshots, opts) do + snapshots + |> Enum.map(&pkey_names(&1.attributes)) + |> Enum.uniq() + |> case do + [pkey_names] -> + {pkey_names, []} + + unique_primary_keys -> + unique_primary_key_names = + unique_primary_keys + |> Enum.with_index() + |> Enum.map_join("\n", fn {pkey, index} -> + "#{index}: #{inspect(pkey)}" + end) + + choice = + if opts.no_shell? do + raise "Unimplemented: cannot resolve primary key ambiguity without shell input" + else + message = """ + Which primary key should be used for the table `#{snapshot.table}` (enter the number)? + + #{unique_primary_key_names} + """ + + message + |> Mix.shell().prompt() + |> String.to_integer() + end + + identities = + unique_primary_keys + |> List.delete_at(choice) + |> Enum.map(fn pkey_names -> + pkey_name_string = Enum.join(pkey_names, "_") + name = snapshot.table <> "_" <> pkey_name_string + + %{ + keys: pkey_names, + name: name + } + end) + + primary_key = Enum.sort(Enum.at(unique_primary_keys, choice)) + + identities = + Enum.reject(identities, fn identity -> + Enum.sort(identity.keys) == primary_key + end) + + {primary_key, identities} + end + end + + defp merge_primary_keys(existing_snapshot, snapshots, opts) do + pkey_names = pkey_names(existing_snapshot.attributes) + + one_pkey_exists? = + Enum.any?(snapshots, fn snapshot -> + pkey_names(snapshot.attributes) == pkey_names + end) + + if one_pkey_exists? do + identities = + snapshots + |> Enum.map(&pkey_names(&1.attributes)) + |> Enum.uniq() + |> Enum.reject(&(&1 == pkey_names)) + |> Enum.map(fn pkey_names -> + pkey_name_string = Enum.join(pkey_names, "_") + name = existing_snapshot.table <> "_" <> pkey_name_string + + %{ + keys: pkey_names, + name: name + } + end) + + {pkey_names, identities} + else + merge_primary_keys(nil, snapshots, opts) + end + end + + defp pkey_names(attributes) do + attributes + |> Enum.filter(& &1.primary_key?) + |> Enum.map(& &1.source) + |> Enum.sort() + end + + defp migration_path(opts, repo) do + repo_name = repo_name(repo) + # Copied from ecto's mix task, thanks Ecto â¤ï¸ + config = repo.config() + app = Keyword.fetch!(config, :otp_app) + + if opts.migration_path do + opts.migration_path + else + Path.join([Mix.Project.deps_paths()[app] || File.cwd!(), "priv"]) + end + |> Path.join(repo_name) + |> Path.join("migrations") + end + + defp repo_name(repo) do + repo |> Module.split() |> List.last() |> Macro.underscore() + end + + defp write_migration!({up, down}, repo, opts) do + migration_path = migration_path(opts, repo) + + {migration_name, last_part} = + if opts.name do + {"#{timestamp(true)}_#{opts.name}", "#{opts.name}"} + else + count = + migration_path + |> Path.join("*_migrate_resources*") + |> Path.wildcard() + |> Enum.map(fn path -> + path + |> Path.basename() + |> String.split("_migrate_resources", parts: 2) + |> Enum.at(1) + |> Integer.parse() + |> case do + {integer, _} -> + integer + + _ -> + 0 + end + end) + |> Enum.max(fn -> 0 end) + |> Kernel.+(1) + + {"#{timestamp(true)}_migrate_resources#{count}", "migrate_resources#{count}"} + end + + migration_file = + migration_path + |> Path.join(migration_name <> ".exs") + + module_name = + Module.concat([repo, Migrations, Macro.camelize(last_part)]) + + contents = """ + defmodule #{inspect(module_name)} do + @moduledoc \"\"\" + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_sqlite.generate_migrations` + \"\"\" + + use Ecto.Migration + + def up do + #{up} + end + + def down do + #{down} + end + end + """ + + try do + contents = format(contents, opts) + + if opts.dry_run do + Mix.shell().info(contents) + else + create_file(migration_file, contents) + end + rescue + exception -> + reraise( + """ + Exception while formatting generated code: + #{Exception.format(:error, exception, __STACKTRACE__)} + + Code: + + #{add_line_numbers(contents)} + + To generate it unformatted anyway, but manually fix it, use the `--no-format` option. + """, + __STACKTRACE__ + ) + end + end + + defp add_line_numbers(contents) do + lines = String.split(contents, "\n") + + digits = String.length(to_string(Enum.count(lines))) + + lines + |> Enum.with_index() + |> Enum.map_join("\n", fn {line, index} -> + "#{String.pad_trailing(to_string(index), digits, " ")} | #{line}" + end) + end + + defp create_new_snapshot(snapshots, repo_name, opts) do + unless opts.dry_run do + Enum.each(snapshots, fn snapshot -> + snapshot_binary = snapshot_to_binary(snapshot) + + snapshot_folder = + opts + |> snapshot_path(snapshot.repo) + |> Path.join(repo_name) + + snapshot_file = Path.join(snapshot_folder, "#{snapshot.table}/#{timestamp()}.json") + + File.mkdir_p(Path.dirname(snapshot_file)) + File.write!(snapshot_file, snapshot_binary, []) + + old_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}.json") + + if File.exists?(old_snapshot_folder) do + new_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}/initial.json") + File.rename(old_snapshot_folder, new_snapshot_folder) + end + end) + end + end + + @doc false + def build_up_and_down(phases) do + up = + Enum.map_join(phases, "\n", fn phase -> + phase + |> phase.__struct__.up() + |> Kernel.<>("\n") + |> maybe_comment(phase) + end) + + down = + phases + |> Enum.reverse() + |> Enum.map_join("\n", fn phase -> + phase + |> phase.__struct__.down() + |> Kernel.<>("\n") + |> maybe_comment(phase) + end) + + {up, down} + end + + defp maybe_comment(text, %{commented?: true}) do + text + |> String.split("\n") + |> Enum.map_join("\n", fn line -> + if String.starts_with?(line, "#") do + line + else + "# #{line}" + end + end) + end + + defp maybe_comment(text, _), do: text + + defp format(string, opts) do + if opts.format do + Code.format_string!(string, locals_without_parens: ecto_sql_locals_without_parens()) + else + string + end + rescue + exception -> + IO.puts(""" + Exception while formatting: + + #{inspect(exception)} + + #{inspect(string)} + """) + + reraise exception, __STACKTRACE__ + end + + defp ecto_sql_locals_without_parens do + path = File.cwd!() |> Path.join("deps/ecto_sql/.formatter.exs") + + if File.exists?(path) do + {opts, _} = Code.eval_file(path) + Keyword.get(opts, :locals_without_parens, []) + else + [] + end + end + + defp streamline(ops, acc \\ []) + defp streamline([], acc), do: Enum.reverse(acc) + + defp streamline( + [ + %Operation.AddAttribute{ + attribute: %{ + source: name + }, + table: table + } = add + | rest + ], + acc + ) do + rest + |> Enum.take_while(fn + %custom{} when custom in [Operation.AddCustomStatement, Operation.RemoveCustomStatement] -> + false + + op -> + op.table == table + end) + |> Enum.with_index() + |> Enum.find(fn + {%Operation.AlterAttribute{ + new_attribute: %{source: ^name, references: references}, + old_attribute: %{source: ^name} + }, _} + when not is_nil(references) -> + true + + _ -> + false + end) + |> case do + nil -> + streamline(rest, [add | acc]) + + {alter, index} -> + new_attribute = Map.put(add.attribute, :references, alter.new_attribute.references) + streamline(List.delete_at(rest, index), [%{add | attribute: new_attribute} | acc]) + end + end + + defp streamline([first | rest], acc) do + streamline(rest, [first | acc]) + end + + defp group_into_phases(ops, current \\ nil, acc \\ []) + + defp group_into_phases([], nil, acc), do: Enum.reverse(acc) + + defp group_into_phases([], phase, acc) do + phase = %{phase | operations: Enum.reverse(phase.operations)} + Enum.reverse([phase | acc]) + end + + defp group_into_phases( + [ + %Operation.CreateTable{table: table, multitenancy: multitenancy} | rest + ], + nil, + acc + ) do + # this is kind of a hack + {has_to_be_in_this_phase, rest} = + Enum.split_with(rest, fn + %Operation.AddAttribute{table: ^table} -> true + _ -> false + end) + + group_into_phases( + rest, + %Phase.Create{ + table: table, + multitenancy: multitenancy, + operations: has_to_be_in_this_phase + }, + acc + ) + end + + defp group_into_phases( + [%Operation.AddAttribute{table: table} = op | rest], + %{table: table} = phase, + acc + ) do + group_into_phases(rest, %{phase | operations: [op | phase.operations]}, acc) + end + + defp group_into_phases( + [%Operation.AlterAttribute{table: table} = op | rest], + %Phase.Alter{table: table} = phase, + acc + ) do + group_into_phases(rest, %{phase | operations: [op | phase.operations]}, acc) + end + + defp group_into_phases( + [%Operation.RenameAttribute{table: table} = op | rest], + %Phase.Alter{table: table} = phase, + acc + ) do + group_into_phases(rest, %{phase | operations: [op | phase.operations]}, acc) + end + + defp group_into_phases( + [%Operation.RemoveAttribute{table: table} = op | rest], + %{table: table} = phase, + acc + ) do + group_into_phases(rest, %{phase | operations: [op | phase.operations]}, acc) + end + + defp group_into_phases([%{no_phase: true} = op | rest], nil, acc) do + group_into_phases(rest, nil, [op | acc]) + end + + defp group_into_phases([operation | rest], nil, acc) do + phase = %Phase.Alter{ + operations: [operation], + multitenancy: operation.multitenancy, + table: operation.table + } + + group_into_phases(rest, phase, acc) + end + + defp group_into_phases(operations, phase, acc) do + phase = %{phase | operations: Enum.reverse(phase.operations)} + group_into_phases(operations, nil, [phase | acc]) + end + + defp sort_operations(ops, acc \\ []) + defp sort_operations([], acc), do: acc + + defp sort_operations([op | rest], []), do: sort_operations(rest, [op]) + + defp sort_operations([op | rest], acc) do + acc = Enum.reverse(acc) + + after_index = Enum.find_index(acc, &after?(op, &1)) + + new_acc = + if after_index do + acc + |> List.insert_at(after_index, op) + |> Enum.reverse() + else + [op | Enum.reverse(acc)] + end + + sort_operations(rest, new_acc) + end + + defp after?(_, %Operation.AlterDeferrability{direction: :down}), do: true + defp after?(%Operation.AlterDeferrability{direction: :up}, _), do: true + + defp after?( + %Operation.RemovePrimaryKey{}, + %Operation.DropForeignKey{} + ), + do: true + + defp after?( + %Operation.DropForeignKey{}, + %Operation.RemovePrimaryKey{} + ), + do: false + + defp after?(%Operation.RemovePrimaryKey{}, _), do: false + defp after?(_, %Operation.RemovePrimaryKey{}), do: true + defp after?(%Operation.RemovePrimaryKeyDown{}, _), do: true + defp after?(_, %Operation.RemovePrimaryKeyDown{}), do: false + + defp after?( + %Operation.AddCustomStatement{}, + _ + ), + do: true + + defp after?( + _, + %Operation.RemoveCustomStatement{} + ), + do: true + + defp after?( + %Operation.AddAttribute{attribute: %{order: l}, table: table}, + %Operation.AddAttribute{attribute: %{order: r}, table: table} + ), + do: l > r + + defp after?( + %Operation.RenameUniqueIndex{ + table: table + }, + %{table: table} + ) do + true + end + + defp after?( + %Operation.AddUniqueIndex{ + table: table + }, + %{table: table} + ) do + true + end + + defp after?( + %Operation.AddCustomIndex{ + table: table + }, + %Operation.AddAttribute{table: table} + ) do + true + end + + defp after?( + %Operation.RemoveUniqueIndex{table: table}, + %Operation.AddUniqueIndex{table: table} + ) do + false + end + + defp after?( + %Operation.RemoveUniqueIndex{table: table}, + %{table: table} + ) do + true + end + + defp after?(%Operation.AlterAttribute{table: table}, %Operation.DropForeignKey{ + table: table, + direction: :up + }), + do: true + + defp after?( + %Operation.AlterAttribute{table: table}, + %Operation.DropForeignKey{ + table: table, + direction: :down + } + ), + do: false + + defp after?( + %Operation.DropForeignKey{ + table: table, + direction: :down + }, + %Operation.AlterAttribute{table: table} + ), + do: true + + defp after?(%Operation.AddAttribute{table: table}, %Operation.CreateTable{ + table: table + }) do + true + end + + defp after?( + %Operation.AddAttribute{ + attribute: %{ + references: %{table: table, destination_attribute: name} + } + }, + %Operation.AddAttribute{table: table, attribute: %{source: name}} + ), + do: true + + defp after?( + %Operation.AddAttribute{ + table: table, + attribute: %{ + primary_key?: false + } + }, + %Operation.AddAttribute{table: table, attribute: %{primary_key?: true}} + ), + do: true + + defp after?( + %Operation.AddAttribute{ + table: table, + attribute: %{ + primary_key?: true + } + }, + %Operation.RemoveAttribute{ + table: table, + attribute: %{primary_key?: true} + } + ), + do: true + + defp after?( + %Operation.AddAttribute{ + table: table, + attribute: %{ + primary_key?: true + } + }, + %Operation.AlterAttribute{ + table: table, + new_attribute: %{primary_key?: false}, + old_attribute: %{primary_key?: true} + } + ), + do: true + + defp after?( + %Operation.AddAttribute{ + table: table, + attribute: %{ + primary_key?: true + } + }, + %Operation.AlterAttribute{ + table: table, + new_attribute: %{primary_key?: false}, + old_attribute: %{primary_key?: true} + } + ), + do: true + + defp after?( + %Operation.RemoveAttribute{ + table: table, + attribute: %{primary_key?: true} + }, + %Operation.AlterAttribute{ + table: table, + new_attribute: %{ + primary_key?: true + }, + old_attribute: %{ + primary_key?: false + } + } + ), + do: true + + defp after?( + %Operation.AlterAttribute{ + table: table, + new_attribute: %{primary_key?: false}, + old_attribute: %{ + primary_key?: true + } + }, + %Operation.AlterAttribute{ + table: table, + new_attribute: %{ + primary_key?: true + }, + old_attribute: %{ + primary_key?: false + } + } + ), + do: true + + defp after?( + %Operation.AlterAttribute{ + table: table, + new_attribute: %{primary_key?: false}, + old_attribute: %{ + primary_key?: true + } + }, + %Operation.AddAttribute{ + table: table, + attribute: %{ + primary_key?: true + } + } + ), + do: false + + defp after?( + %Operation.AlterAttribute{ + table: table, + new_attribute: %{primary_key?: false}, + old_attribute: %{primary_key?: true} + }, + %Operation.AddAttribute{ + table: table, + attribute: %{ + primary_key?: true + } + } + ), + do: true + + defp after?( + %Operation.AlterAttribute{ + new_attribute: %{ + references: %{destination_attribute: destination_attribute, table: table} + } + }, + %Operation.AddUniqueIndex{identity: %{keys: keys}, table: table} + ) do + destination_attribute in keys + end + + defp after?( + %Operation.AlterAttribute{ + new_attribute: %{references: %{table: table, destination_attribute: source}} + }, + %Operation.AlterAttribute{ + new_attribute: %{ + source: source + }, + table: table + } + ) do + true + end + + defp after?( + %Operation.AlterAttribute{ + new_attribute: %{ + source: source + }, + table: table + }, + %Operation.AlterAttribute{ + new_attribute: %{references: %{table: table, destination_attribute: source}} + } + ) do + false + end + + defp after?( + %Operation.RemoveAttribute{attribute: %{source: source}, table: table}, + %Operation.AlterAttribute{ + old_attribute: %{ + references: %{table: table, destination_attribute: source} + } + } + ), + do: true + + defp after?( + %Operation.AlterAttribute{ + new_attribute: %{ + references: %{table: table, destination_attribute: name} + } + }, + %Operation.AddAttribute{table: table, attribute: %{source: name}} + ), + do: true + + defp after?( + %Operation.AlterAttribute{new_attribute: %{references: references}, table: table}, + %{table: table} + ) + when not is_nil(references), + do: true + + defp after?(_, _), do: false + + defp fetch_operations(snapshots, opts) do + snapshots + |> Enum.map(fn {snapshot, existing_snapshot} -> + {snapshot, do_fetch_operations(snapshot, existing_snapshot, opts)} + end) + |> Enum.reject(fn {_, ops} -> + Enum.empty?(ops) + end) + end + + defp do_fetch_operations(snapshot, existing_snapshot, opts, acc \\ []) + + defp do_fetch_operations(snapshot, nil, opts, acc) do + empty_snapshot = %{ + attributes: [], + identities: [], + custom_indexes: [], + custom_statements: [], + table: snapshot.table, + repo: snapshot.repo, + base_filter: nil, + empty?: true, + multitenancy: %{ + attribute: nil, + strategy: nil, + global: nil + } + } + + do_fetch_operations(snapshot, empty_snapshot, opts, [ + %Operation.CreateTable{ + table: snapshot.table, + multitenancy: snapshot.multitenancy, + old_multitenancy: empty_snapshot.multitenancy + } + | acc + ]) + end + + defp do_fetch_operations(snapshot, old_snapshot, opts, acc) do + attribute_operations = attribute_operations(snapshot, old_snapshot, opts) + pkey_operations = pkey_operations(snapshot, old_snapshot, attribute_operations) + + rewrite_all_identities? = changing_multitenancy_affects_identities?(snapshot, old_snapshot) + + custom_statements_to_add = + snapshot.custom_statements + |> Enum.reject(fn statement -> + Enum.any?(old_snapshot.custom_statements, &(&1.name == statement.name)) + end) + |> Enum.map(&%Operation.AddCustomStatement{statement: &1, table: snapshot.table}) + + custom_statements_to_remove = + old_snapshot.custom_statements + |> Enum.reject(fn old_statement -> + Enum.any?(snapshot.custom_statements, &(&1.name == old_statement.name)) + end) + |> Enum.map(&%Operation.RemoveCustomStatement{statement: &1, table: snapshot.table}) + + custom_statements_to_alter = + snapshot.custom_statements + |> Enum.flat_map(fn statement -> + old_statement = Enum.find(old_snapshot.custom_statements, &(&1.name == statement.name)) + + if old_statement && + (old_statement.code? != statement.code? || + old_statement.up != statement.up || old_statement.down != statement.down) do + [ + %Operation.RemoveCustomStatement{statement: old_statement, table: snapshot.table}, + %Operation.AddCustomStatement{statement: statement, table: snapshot.table} + ] + else + [] + end + end) + + custom_indexes_to_add = + Enum.filter(snapshot.custom_indexes, fn index -> + !Enum.find(old_snapshot.custom_indexes, fn old_custom_index -> + indexes_match?(snapshot.table, old_custom_index, index) + end) + end) + |> Enum.map(fn custom_index -> + %Operation.AddCustomIndex{ + index: custom_index, + table: snapshot.table, + multitenancy: snapshot.multitenancy, + base_filter: snapshot.base_filter + } + end) + + custom_indexes_to_remove = + Enum.filter(old_snapshot.custom_indexes, fn old_custom_index -> + rewrite_all_identities? || + !Enum.find(snapshot.custom_indexes, fn index -> + indexes_match?(snapshot.table, old_custom_index, index) + end) + end) + |> Enum.map(fn custom_index -> + %Operation.RemoveCustomIndex{ + index: custom_index, + table: old_snapshot.table, + multitenancy: old_snapshot.multitenancy, + base_filter: old_snapshot.base_filter + } + end) + + unique_indexes_to_remove = + if rewrite_all_identities? do + old_snapshot.identities + else + Enum.reject(old_snapshot.identities, fn old_identity -> + Enum.find(snapshot.identities, fn identity -> + identity.name == old_identity.name && + Enum.sort(old_identity.keys) == Enum.sort(identity.keys) && + old_identity.base_filter == identity.base_filter + end) + end) + end + |> Enum.map(fn identity -> + %Operation.RemoveUniqueIndex{ + identity: identity, + table: snapshot.table + } + end) + + unique_indexes_to_rename = + if rewrite_all_identities? do + [] + else + snapshot.identities + |> Enum.map(fn identity -> + Enum.find_value(old_snapshot.identities, fn old_identity -> + if old_identity.name == identity.name && + old_identity.index_name != identity.index_name do + {old_identity, identity} + end + end) + end) + |> Enum.filter(& &1) + end + |> Enum.map(fn {old_identity, new_identity} -> + %Operation.RenameUniqueIndex{ + old_identity: old_identity, + new_identity: new_identity, + table: snapshot.table + } + end) + + unique_indexes_to_add = + if rewrite_all_identities? do + snapshot.identities + else + Enum.reject(snapshot.identities, fn identity -> + Enum.find(old_snapshot.identities, fn old_identity -> + old_identity.name == identity.name && + Enum.sort(old_identity.keys) == Enum.sort(identity.keys) && + old_identity.base_filter == identity.base_filter + end) + end) + end + |> Enum.map(fn identity -> + %Operation.AddUniqueIndex{ + identity: identity, + table: snapshot.table + } + end) + + [ + pkey_operations, + unique_indexes_to_remove, + attribute_operations, + unique_indexes_to_add, + unique_indexes_to_rename, + custom_indexes_to_add, + custom_indexes_to_remove, + custom_statements_to_add, + custom_statements_to_remove, + custom_statements_to_alter, + acc + ] + |> Enum.concat() + |> Enum.map(&Map.put(&1, :multitenancy, snapshot.multitenancy)) + |> Enum.map(&Map.put(&1, :old_multitenancy, old_snapshot.multitenancy)) + end + + defp indexes_match?(table, left, right) do + left = + left + |> Map.update!(:fields, fn fields -> + Enum.map(fields, &to_string/1) + end) + |> add_custom_index_name(table) + + right = + right + |> Map.update!(:fields, fn fields -> + Enum.map(fields, &to_string/1) + end) + |> add_custom_index_name(table) + + left == right + end + + defp add_custom_index_name(custom_index, table) do + custom_index + |> Map.put_new_lazy(:name, fn -> + AshSqlite.CustomIndex.name(table, %{fields: custom_index.fields}) + end) + |> Map.update!( + :name, + &(&1 || AshSqlite.CustomIndex.name(table, %{fields: custom_index.fields})) + ) + end + + defp pkey_operations(snapshot, old_snapshot, attribute_operations) do + if old_snapshot[:empty?] do + [] + else + must_drop_pkey? = + Enum.any?( + attribute_operations, + fn + %Operation.AlterAttribute{ + old_attribute: %{primary_key?: old_primary_key}, + new_attribute: %{primary_key?: new_primary_key} + } + when old_primary_key != new_primary_key -> + true + + %Operation.AddAttribute{ + attribute: %{primary_key?: true} + } -> + true + + _ -> + false + end + ) + + if must_drop_pkey? do + [ + %Operation.RemovePrimaryKey{table: snapshot.table}, + %Operation.RemovePrimaryKeyDown{table: snapshot.table} + ] + else + [] + end + end + end + + defp attribute_operations(snapshot, old_snapshot, opts) do + attributes_to_add = + Enum.reject(snapshot.attributes, fn attribute -> + Enum.find(old_snapshot.attributes, &(&1.source == attribute.source)) + end) + + attributes_to_remove = + Enum.reject(old_snapshot.attributes, fn attribute -> + Enum.find(snapshot.attributes, &(&1.source == attribute.source)) + end) + + {attributes_to_add, attributes_to_remove, attributes_to_rename} = + resolve_renames(snapshot.table, attributes_to_add, attributes_to_remove, opts) + + attributes_to_alter = + snapshot.attributes + |> Enum.map(fn attribute -> + {attribute, + Enum.find( + old_snapshot.attributes, + &(&1.source == attribute.source && + attributes_unequal?(&1, attribute, snapshot.repo, old_snapshot, snapshot)) + )} + end) + |> Enum.filter(&elem(&1, 1)) + + rename_attribute_events = + Enum.map(attributes_to_rename, fn {new, old} -> + %Operation.RenameAttribute{ + new_attribute: new, + old_attribute: old, + table: snapshot.table + } + end) + + add_attribute_events = + Enum.flat_map(attributes_to_add, fn attribute -> + if attribute.references do + [ + %Operation.AddAttribute{ + attribute: attribute, + table: snapshot.table + }, + %Operation.DropForeignKey{ + attribute: attribute, + table: snapshot.table, + multitenancy: Map.get(attribute, :multitenancy), + direction: :down + } + ] + else + [ + %Operation.AddAttribute{ + attribute: attribute, + table: snapshot.table + } + ] + end + end) + + alter_attribute_events = + Enum.flat_map(attributes_to_alter, fn {new_attribute, old_attribute} -> + deferrable_ops = + if differently_deferrable?(new_attribute, old_attribute) do + [ + %Operation.AlterDeferrability{ + table: snapshot.table, + references: new_attribute.references, + direction: :up + }, + %Operation.AlterDeferrability{ + table: snapshot.table, + references: Map.get(old_attribute, :references), + direction: :down + } + ] + else + [] + end + + if has_reference?(old_snapshot.multitenancy, old_attribute) and + Map.get(old_attribute, :references) != Map.get(new_attribute, :references) do + redo_deferrability = + if differently_deferrable?(new_attribute, old_attribute) do + [] + else + [ + %Operation.AlterDeferrability{ + table: snapshot.table, + references: new_attribute.references, + direction: :up + } + ] + end + + old_and_alter = + [ + %Operation.DropForeignKey{ + attribute: old_attribute, + table: snapshot.table, + multitenancy: old_snapshot.multitenancy, + direction: :up + }, + %Operation.AlterAttribute{ + new_attribute: new_attribute, + old_attribute: old_attribute, + table: snapshot.table + } + ] ++ redo_deferrability + + if has_reference?(snapshot.multitenancy, new_attribute) do + reference_ops = [ + %Operation.DropForeignKey{ + attribute: new_attribute, + table: snapshot.table, + multitenancy: snapshot.multitenancy, + direction: :down + } + ] + + old_and_alter ++ + reference_ops + else + old_and_alter + end + else + [ + %Operation.AlterAttribute{ + new_attribute: Map.delete(new_attribute, :references), + old_attribute: Map.delete(old_attribute, :references), + table: snapshot.table + } + ] + end + |> Enum.concat(deferrable_ops) + end) + + remove_attribute_events = + Enum.map(attributes_to_remove, fn attribute -> + %Operation.RemoveAttribute{ + attribute: attribute, + table: snapshot.table, + commented?: !opts.drop_columns + } + end) + + add_attribute_events ++ + alter_attribute_events ++ remove_attribute_events ++ rename_attribute_events + end + + defp differently_deferrable?(%{references: %{deferrable: left}}, %{ + references: %{deferrable: right} + }) + when left != right do + true + end + + defp differently_deferrable?(%{references: %{deferrable: same}}, %{ + references: %{deferrable: same} + }) do + false + end + + defp differently_deferrable?(%{references: %{deferrable: left}}, _) when left != false, do: true + + defp differently_deferrable?(_, %{references: %{deferrable: right}}) when right != false, + do: true + + defp differently_deferrable?(_, _), do: false + + # This exists to handle the fact that the remapping of the key name -> source caused attributes + # to be considered unequal. We ignore things that only differ in that way using this function. + defp attributes_unequal?(left, right, repo, _old_snapshot, _new_snapshot) do + left = clean_for_equality(left, repo) + + right = clean_for_equality(right, repo) + + left != right + end + + defp clean_for_equality(attribute, _repo) do + cond do + attribute[:source] -> + Map.put(attribute, :name, attribute[:source]) + |> Map.update!(:source, &to_string/1) + |> Map.update!(:name, &to_string/1) + + attribute[:name] -> + attribute + |> Map.put(:source, attribute[:name]) + |> Map.update!(:source, &to_string/1) + |> Map.update!(:name, &to_string/1) + + true -> + attribute + end + |> add_ignore() + |> then(fn + # only :integer cares about `destination_attribute_generated` + # so we clean it here to avoid generating unnecessary snapshots + # during the transitionary period of adding it + %{type: type, references: references} = attribute + when not is_nil(references) and type != :integer -> + Map.update!(attribute, :references, &Map.delete(&1, :destination_attribute_generated)) + + attribute -> + attribute + end) + end + + defp add_ignore(%{references: references} = attribute) when is_map(references) do + %{attribute | references: Map.put_new(references, :ignore?, false)} + end + + defp add_ignore(attribute) do + attribute + end + + def changing_multitenancy_affects_identities?(snapshot, old_snapshot) do + snapshot.multitenancy != old_snapshot.multitenancy || + snapshot.base_filter != old_snapshot.base_filter + end + + def has_reference?(_multitenancy, attribute) do + not is_nil(Map.get(attribute, :references)) + end + + def get_existing_snapshot(snapshot, opts) do + repo_name = snapshot.repo |> Module.split() |> List.last() |> Macro.underscore() + + folder = + opts + |> snapshot_path(snapshot.repo) + |> Path.join(repo_name) + + snapshot_folder = Path.join(folder, snapshot.table) + + if File.exists?(snapshot_folder) do + snapshot_folder + |> File.ls!() + |> Enum.filter(&String.ends_with?(&1, ".json")) + |> Enum.map(&String.trim_trailing(&1, ".json")) + |> Enum.map(&Integer.parse/1) + |> Enum.filter(fn + {_int, remaining} -> + remaining == "" + + :error -> + false + end) + |> Enum.map(&elem(&1, 0)) + |> case do + [] -> + get_old_snapshot(folder, snapshot) + + timestamps -> + timestamp = Enum.max(timestamps) + snapshot_file = Path.join(snapshot_folder, "#{timestamp}.json") + + snapshot_file + |> File.read!() + |> load_snapshot() + end + else + get_old_snapshot(folder, snapshot) + end + end + + defp get_old_snapshot(folder, snapshot) do + old_snapshot_file = Path.join(folder, "#{snapshot.table}.json") + # This is adapter code for the old version, where migrations were stored in a flat directory + if File.exists?(old_snapshot_file) do + old_snapshot_file + |> File.read!() + |> load_snapshot() + end + end + + defp resolve_renames(_table, adding, [], _opts), do: {adding, [], []} + + defp resolve_renames(_table, [], removing, _opts), do: {[], removing, []} + + defp resolve_renames(table, [adding], [removing], opts) do + if renaming_to?(table, removing.source, adding.source, opts) do + {[], [], [{adding, removing}]} + else + {[adding], [removing], []} + end + end + + defp resolve_renames(table, adding, [removing | rest], opts) do + {new_adding, new_removing, new_renames} = + if renaming?(table, removing, opts) do + new_attribute = + if opts.no_shell? do + raise "Unimplemented: Cannot get new_attribute without the shell!" + else + get_new_attribute(adding) + end + + {adding -- [new_attribute], [], [{new_attribute, removing}]} + else + {adding, [removing], []} + end + + {rest_adding, rest_removing, rest_renames} = resolve_renames(table, new_adding, rest, opts) + + {new_adding ++ rest_adding, new_removing ++ rest_removing, rest_renames ++ new_renames} + end + + defp renaming_to?(table, removing, adding, opts) do + if opts.no_shell? do + raise "Unimplemented: cannot determine: Are you renaming #{table}.#{removing} to #{table}.#{adding}? without shell input" + else + Mix.shell().yes?("Are you renaming #{table}.#{removing} to #{table}.#{adding}?") + end + end + + defp renaming?(table, removing, opts) do + if opts.no_shell? do + raise "Unimplemented: cannot determine: Are you renaming #{table}.#{removing.source}? without shell input" + else + Mix.shell().yes?("Are you renaming #{table}.#{removing.source}?") + end + end + + defp get_new_attribute(adding, tries \\ 3) + + defp get_new_attribute(_adding, 0) do + raise "Could not get matching name after 3 attempts." + end + + defp get_new_attribute(adding, tries) do + name = + Mix.shell().prompt( + "What are you renaming it to?: #{Enum.map_join(adding, ", ", & &1.source)}" + ) + + name = + if name do + String.trim(name) + else + nil + end + + case Enum.find(adding, &(to_string(&1.source) == name)) do + nil -> get_new_attribute(adding, tries - 1) + new_attribute -> new_attribute + end + end + + defp timestamp(require_unique? \\ false) do + # Alright, this is silly I know. But migration ids need to be unique + # and "synthesizing" that behavior is significantly more annoying than + # just waiting a bit, ensuring the migration versions are unique. + if require_unique?, do: :timer.sleep(1500) + {{y, m, d}, {hh, mm, ss}} = :calendar.universal_time() + "#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}" + end + + defp pad(i) when i < 10, do: <> + defp pad(i), do: to_string(i) + + def get_snapshots(resource, all_resources) do + Code.ensure_compiled!(AshSqlite.DataLayer.Info.repo(resource)) + + if AshSqlite.DataLayer.Info.polymorphic?(resource) do + all_resources + |> Enum.flat_map(&Ash.Resource.Info.relationships/1) + |> Enum.filter(&(&1.destination == resource)) + |> Enum.reject(&(&1.type == :belongs_to)) + |> Enum.filter(& &1.context[:data_layer][:table]) + |> Enum.uniq() + |> Enum.map(fn relationship -> + resource + |> do_snapshot(relationship.context[:data_layer][:table]) + |> Map.update!(:identities, fn identities -> + identity_index_names = AshSqlite.DataLayer.Info.identity_index_names(resource) + + Enum.map(identities, fn identity -> + Map.put( + identity, + :index_name, + identity_index_names[identity.name] || + "#{relationship.context[:data_layer][:table]}_#{identity.name}_index" + ) + end) + end) + |> Map.update!(:attributes, fn attributes -> + Enum.map(attributes, fn attribute -> + destination_attribute_source = + relationship.destination + |> Ash.Resource.Info.attribute(relationship.destination_attribute) + |> Map.get(:source) + + if attribute.source == destination_attribute_source do + source_attribute = + Ash.Resource.Info.attribute(relationship.source, relationship.source_attribute) + + Map.put(attribute, :references, %{ + destination_attribute: source_attribute.source, + destination_attribute_default: + default( + source_attribute, + relationship.destination, + AshSqlite.DataLayer.Info.repo(relationship.destination) + ), + deferrable: false, + destination_attribute_generated: source_attribute.generated?, + multitenancy: multitenancy(relationship.source), + table: AshSqlite.DataLayer.Info.table(relationship.source), + on_delete: AshSqlite.DataLayer.Info.polymorphic_on_delete(relationship.source), + on_update: AshSqlite.DataLayer.Info.polymorphic_on_update(relationship.source), + primary_key?: source_attribute.primary_key?, + name: + AshSqlite.DataLayer.Info.polymorphic_name(relationship.source) || + "#{relationship.context[:data_layer][:table]}_#{destination_attribute_source}_fkey" + }) + else + attribute + end + end) + end) + end) + else + [do_snapshot(resource, AshSqlite.DataLayer.Info.table(resource))] + end + end + + defp do_snapshot(resource, table) do + snapshot = %{ + attributes: attributes(resource, table), + identities: identities(resource), + table: table || AshSqlite.DataLayer.Info.table(resource), + custom_indexes: custom_indexes(resource), + custom_statements: custom_statements(resource), + repo: AshSqlite.DataLayer.Info.repo(resource), + multitenancy: multitenancy(resource), + base_filter: AshSqlite.DataLayer.Info.base_filter_sql(resource), + has_create_action: has_create_action?(resource) + } + + hash = + :sha256 + |> :crypto.hash(inspect(snapshot)) + |> Base.encode16() + + Map.put(snapshot, :hash, hash) + end + + defp has_create_action?(resource) do + resource + |> Ash.Resource.Info.actions() + |> Enum.any?(&(&1.type == :create && !&1.manual)) + end + + defp custom_indexes(resource) do + resource + |> AshSqlite.DataLayer.Info.custom_indexes() + |> Enum.map(fn custom_index -> + Map.take(custom_index, AshSqlite.CustomIndex.fields()) + end) + end + + defp custom_statements(resource) do + resource + |> AshSqlite.DataLayer.Info.custom_statements() + |> Enum.map(fn custom_statement -> + Map.take(custom_statement, AshSqlite.Statement.fields()) + end) + end + + defp multitenancy(resource) do + strategy = Ash.Resource.Info.multitenancy_strategy(resource) + attribute = Ash.Resource.Info.multitenancy_attribute(resource) + global = Ash.Resource.Info.multitenancy_global?(resource) + + %{ + strategy: strategy, + attribute: attribute, + global: global + } + end + + defp attributes(resource, table) do + repo = AshSqlite.DataLayer.Info.repo(resource) + ignored = AshSqlite.DataLayer.Info.migration_ignore_attributes(resource) || [] + + resource + |> Ash.Resource.Info.attributes() + |> Enum.reject(&(&1.name in ignored)) + |> Enum.map( + &Map.take(&1, [ + :name, + :source, + :type, + :default, + :allow_nil?, + :generated?, + :primary_key?, + :constraints + ]) + ) + |> Enum.map(fn attribute -> + default = default(attribute, resource, repo) + + type = + AshSqlite.DataLayer.Info.migration_types(resource)[attribute.name] || + migration_type(attribute.type, attribute.constraints) + + type = + if :erlang.function_exported(repo, :override_migration_type, 1) do + repo.override_migration_type(type) + else + type + end + + {type, size} = + case type do + {:varchar, size} -> + {:varchar, size} + + {:binary, size} -> + {:binary, size} + + {other, size} when is_atom(other) and is_integer(size) -> + {other, size} + + other -> + {other, nil} + end + + attribute + |> Map.put(:default, default) + |> Map.put(:size, size) + |> Map.put(:type, type) + |> Map.put(:source, attribute.source || attribute.name) + |> Map.drop([:name, :constraints]) + end) + |> Enum.map(fn attribute -> + references = find_reference(resource, table, attribute) + + Map.put(attribute, :references, references) + end) + end + + defp find_reference(resource, table, attribute) do + Enum.find_value(Ash.Resource.Info.relationships(resource), fn relationship -> + source_attribute_name = + relationship.source + |> Ash.Resource.Info.attribute(relationship.source_attribute) + |> then(fn attribute -> + attribute.source || attribute.name + end) + + if attribute.source == source_attribute_name && relationship.type == :belongs_to && + foreign_key?(relationship) do + configured_reference = + configured_reference(resource, table, attribute.source || attribute.name, relationship) + + unless Map.get(configured_reference, :ignore?) do + destination_attribute = + Ash.Resource.Info.attribute( + relationship.destination, + relationship.destination_attribute + ) + + destination_attribute_source = + destination_attribute.source || destination_attribute.name + + %{ + destination_attribute: destination_attribute_source, + deferrable: configured_reference.deferrable, + multitenancy: multitenancy(relationship.destination), + on_delete: configured_reference.on_delete, + on_update: configured_reference.on_update, + name: configured_reference.name, + primary_key?: destination_attribute.primary_key?, + table: + relationship.context[:data_layer][:table] || + AshSqlite.DataLayer.Info.table(relationship.destination) + } + end + end + end) + end + + defp configured_reference(resource, table, attribute, relationship) do + ref = + resource + |> AshSqlite.DataLayer.Info.references() + |> Enum.find(&(&1.relationship == relationship.name)) + |> Kernel.||(%{ + on_delete: nil, + on_update: nil, + deferrable: false, + name: nil, + ignore?: false + }) + + ref + |> Map.put(:name, ref.name || "#{table}_#{attribute}_fkey") + |> Map.put( + :primary_key?, + Ash.Resource.Info.attribute( + relationship.destination, + relationship.destination_attribute + ).primary_key? + ) + end + + def get_migration_type(type, constraints), do: migration_type(type, constraints) + + defp migration_type({:array, type}, constraints), + do: {:array, migration_type(type, constraints)} + + defp migration_type(Ash.Type.CiString, _), do: :citext + defp migration_type(Ash.Type.UUID, _), do: :uuid + defp migration_type(Ash.Type.Integer, _), do: :bigint + + defp migration_type(other, constraints) do + type = Ash.Type.get_type(other) + + migration_type_from_storage_type(Ash.Type.storage_type(type, constraints)) + end + + defp migration_type_from_storage_type(:string), do: :text + defp migration_type_from_storage_type(:ci_string), do: :citext + defp migration_type_from_storage_type(storage_type), do: storage_type + + defp foreign_key?(relationship) do + Ash.DataLayer.data_layer(relationship.source) == AshSqlite.DataLayer && + AshSqlite.DataLayer.Info.repo(relationship.source) == + AshSqlite.DataLayer.Info.repo(relationship.destination) + end + + defp identities(resource) do + identity_index_names = AshSqlite.DataLayer.Info.identity_index_names(resource) + + resource + |> Ash.Resource.Info.identities() + |> case do + [] -> + [] + + identities -> + base_filter = Ash.Resource.Info.base_filter(resource) + + if base_filter && !AshSqlite.DataLayer.Info.base_filter_sql(resource) do + raise """ + Cannot create a unique index for a resource with a base filter without also configuring `base_filter_sql`. + + You must provide the `base_filter_sql` option, or skip unique indexes with `skip_unique_indexes`" + """ + end + + identities + end + |> Enum.reject(fn identity -> + identity.name in AshSqlite.DataLayer.Info.skip_unique_indexes(resource) + end) + |> Enum.filter(fn identity -> + Enum.all?(identity.keys, fn key -> + Ash.Resource.Info.attribute(resource, key) + end) + end) + |> Enum.sort_by(& &1.name) + |> Enum.map(&Map.take(&1, [:name, :keys])) + |> Enum.map(fn %{keys: keys} = identity -> + %{ + identity + | keys: + Enum.map(keys, fn key -> + attribute = Ash.Resource.Info.attribute(resource, key) + attribute.source || attribute.name + end) + } + end) + |> Enum.map(fn identity -> + Map.put( + identity, + :index_name, + identity_index_names[identity.name] || + "#{AshSqlite.DataLayer.Info.table(resource)}_#{identity.name}_index" + ) + end) + |> Enum.map(&Map.put(&1, :base_filter, AshSqlite.DataLayer.Info.base_filter_sql(resource))) + end + + defp default(%{name: name, default: default}, resource, _repo) when is_function(default) do + configured_default(resource, name) || "nil" + end + + defp default(%{name: name, default: {_, _, _}}, resource, _), + do: configured_default(resource, name) || "nil" + + defp default(%{name: name, default: nil}, resource, _), + do: configured_default(resource, name) || "nil" + + defp default(%{name: name, default: []}, resource, _), + do: configured_default(resource, name) || "[]" + + defp default(%{name: name, default: default}, resource, _) when default == %{}, + do: configured_default(resource, name) || "%{}" + + defp default(%{name: name, default: value, type: type} = attr, resource, _) do + case configured_default(resource, name) do + nil -> + case migration_default(type, Map.get(attr, :constraints, []), value) do + {:ok, default} -> + default + + :error -> + "nil" + end + + default -> + default + end + end + + defp migration_default(type, constraints, value) do + type = + type + |> unwrap_type() + |> Ash.Type.get_type() + + if function_exported?(type, :value_to_sqlite_default, 3) do + type.value_to_sqlite_default(type, constraints, value) + else + :error + end + end + + defp unwrap_type({:array, type}), do: unwrap_type(type) + defp unwrap_type(type), do: type + + defp configured_default(resource, attribute) do + AshSqlite.DataLayer.Info.migration_defaults(resource)[attribute] + end + + defp snapshot_to_binary(snapshot) do + snapshot + |> Map.update!(:attributes, fn attributes -> + Enum.map(attributes, fn attribute -> + %{attribute | type: sanitize_type(attribute.type, attribute[:size])} + end) + end) + |> Jason.encode!(pretty: true) + end + + defp sanitize_type({:array, type}, size) do + ["array", sanitize_type(type, size)] + end + + defp sanitize_type(:varchar, size) when not is_nil(size) do + ["varchar", size] + end + + defp sanitize_type(:binary, size) when not is_nil(size) do + ["binary", size] + end + + defp sanitize_type(type, size) when is_atom(type) and is_integer(size) do + [sanitize_type(type, nil), size] + end + + defp sanitize_type(type, _) do + type + end + + defp load_snapshot(json) do + json + |> Jason.decode!(keys: :atoms!) + |> sanitize_snapshot() + end + + defp sanitize_snapshot(snapshot) do + snapshot + |> Map.put_new(:has_create_action, true) + |> Map.update!(:identities, fn identities -> + Enum.map(identities, &load_identity(&1, snapshot.table)) + end) + |> Map.update!(:attributes, fn attributes -> + Enum.map(attributes, fn attribute -> + attribute = load_attribute(attribute, snapshot.table) + + if is_map(Map.get(attribute, :references)) do + %{ + attribute + | references: rewrite(attribute.references, :ignore, :ignore?) + } + else + attribute + end + end) + end) + |> Map.put_new(:custom_indexes, []) + |> Map.update!(:custom_indexes, &load_custom_indexes/1) + |> Map.put_new(:custom_statements, []) + |> Map.update!(:custom_statements, &load_custom_statements/1) + |> Map.update!(:repo, &String.to_atom/1) + |> Map.put_new(:multitenancy, %{ + attribute: nil, + strategy: nil, + global: nil + }) + |> Map.update!(:multitenancy, &load_multitenancy/1) + |> Map.put_new(:base_filter, nil) + end + + defp load_custom_indexes(custom_indexes) do + Enum.map(custom_indexes || [], fn custom_index -> + custom_index + |> Map.put_new(:fields, []) + |> Map.put_new(:include, []) + |> Map.put_new(:message, nil) + end) + end + + defp load_custom_statements(statements) do + Enum.map(statements || [], fn statement -> + Map.update!(statement, :name, &String.to_atom/1) + end) + end + + defp load_multitenancy(multitenancy) do + multitenancy + |> Map.update!(:strategy, fn strategy -> strategy && String.to_atom(strategy) end) + |> Map.update!(:attribute, fn attribute -> attribute && String.to_atom(attribute) end) + end + + defp load_attribute(attribute, table) do + type = load_type(attribute.type) + + {type, size} = + case type do + {:varchar, size} -> + {:varchar, size} + + {:binary, size} -> + {:binary, size} + + {other, size} when is_atom(other) and is_integer(size) -> + {other, size} + + other -> + {other, nil} + end + + attribute = + if Map.has_key?(attribute, :name) do + Map.put(attribute, :source, String.to_atom(attribute.name)) + else + Map.update!(attribute, :source, &String.to_atom/1) + end + + attribute + |> Map.put(:type, type) + |> Map.put(:size, size) + |> Map.put_new(:default, "nil") + |> Map.update!(:default, &(&1 || "nil")) + |> Map.update!(:references, fn + nil -> + nil + + references -> + references + |> rewrite( + destination_field: :destination_attribute, + destination_field_default: :destination_attribute_default, + destination_field_generated: :destination_attribute_generated + ) + |> Map.delete(:ignore) + |> rewrite(:ignore?, :ignore) + |> Map.update!(:destination_attribute, &String.to_atom/1) + |> Map.put_new(:deferrable, false) + |> Map.update!(:deferrable, fn + "initially" -> :initially + other -> other + end) + |> Map.put_new(:destination_attribute_default, "nil") + |> Map.put_new(:destination_attribute_generated, false) + |> Map.put_new(:on_delete, nil) + |> Map.put_new(:on_update, nil) + |> Map.update!(:on_delete, &(&1 && String.to_atom(&1))) + |> Map.update!(:on_update, &(&1 && String.to_atom(&1))) + |> Map.put( + :name, + Map.get(references, :name) || "#{table}_#{attribute.source}_fkey" + ) + |> Map.put_new(:multitenancy, %{ + attribute: nil, + strategy: nil, + global: nil + }) + |> Map.update!(:multitenancy, &load_multitenancy/1) + |> sanitize_name(table) + end) + end + + defp rewrite(map, keys) do + Enum.reduce(keys, map, fn {key, to}, map -> + rewrite(map, key, to) + end) + end + + defp rewrite(map, key, to) do + if Map.has_key?(map, key) do + map + |> Map.put(to, Map.get(map, key)) + |> Map.delete(key) + else + map + end + end + + defp sanitize_name(reference, table) do + if String.starts_with?(reference.name, "_") do + Map.put(reference, :name, "#{table}#{reference.name}") + else + reference + end + end + + defp load_type(["array", type]) do + {:array, load_type(type)} + end + + defp load_type(["varchar", size]) do + {:varchar, size} + end + + defp load_type(["binary", size]) do + {:binary, size} + end + + defp load_type([string, size]) when is_binary(string) and is_integer(size) do + {String.to_existing_atom(string), size} + end + + defp load_type(type) do + String.to_atom(type) + end + + defp load_identity(identity, table) do + identity + |> Map.update!(:name, &String.to_atom/1) + |> Map.update!(:keys, fn keys -> + keys + |> Enum.map(&String.to_atom/1) + |> Enum.sort() + end) + |> add_index_name(table) + |> Map.put_new(:base_filter, nil) + end + + defp add_index_name(%{name: name} = index, table) do + Map.put_new(index, :index_name, "#{table}_#{name}_unique_index") + end +end diff --git a/lib/migration_generator/operation.ex b/lib/migration_generator/operation.ex new file mode 100644 index 0000000..54d2a8b --- /dev/null +++ b/lib/migration_generator/operation.ex @@ -0,0 +1,784 @@ +defmodule AshSqlite.MigrationGenerator.Operation do + @moduledoc false + + defmodule Helper do + @moduledoc false + def join(list), + do: + list + |> List.flatten() + |> Enum.reject(&is_nil/1) + |> Enum.join(", ") + |> String.replace(", )", ")") + + def maybe_add_default("nil"), do: nil + def maybe_add_default(value), do: "default: #{value}" + + def maybe_add_primary_key(true), do: "primary_key: true" + def maybe_add_primary_key(_), do: nil + + def maybe_add_null(false), do: "null: false" + def maybe_add_null(_), do: nil + + def in_quotes(nil), do: nil + def in_quotes(value), do: "\"#{value}\"" + + def as_atom(value) when is_atom(value), do: Macro.inspect_atom(:remote_call, value) + # sobelow_skip ["DOS.StringToAtom"] + def as_atom(value), do: Macro.inspect_atom(:remote_call, String.to_atom(value)) + + def option(key, value) do + if value do + "#{as_atom(key)}: #{inspect(value)}" + end + end + + def on_delete(%{on_delete: on_delete}) when on_delete in [:delete, :nilify] do + "on_delete: :#{on_delete}_all" + end + + def on_delete(%{on_delete: on_delete}) when is_atom(on_delete) and not is_nil(on_delete) do + "on_delete: :#{on_delete}" + end + + def on_delete(_), do: nil + + def on_update(%{on_update: on_update}) when on_update in [:update, :nilify] do + "on_update: :#{on_update}_all" + end + + def on_update(%{on_update: on_update}) when is_atom(on_update) and not is_nil(on_update) do + "on_update: :#{on_update}" + end + + def on_update(_), do: nil + + def reference_type( + %{type: :integer}, + %{destination_attribute_generated: true, destination_attribute_default: "nil"} + ) do + :bigint + end + + def reference_type(%{type: type}, _) do + type + end + end + + defmodule CreateTable do + @moduledoc false + defstruct [:table, :multitenancy, :old_multitenancy] + end + + defmodule AddAttribute do + @moduledoc false + defstruct [:attribute, :table, :multitenancy, :old_multitenancy] + + import Helper + + def up(%{ + multitenancy: %{strategy: :attribute, attribute: source_attribute}, + attribute: + %{ + references: + %{ + table: table, + destination_attribute: reference_attribute, + multitenancy: %{strategy: :attribute, attribute: destination_attribute} + } = reference + } = attribute + }) do + with_match = + if destination_attribute != reference_attribute do + "with: [#{as_atom(source_attribute)}: :#{as_atom(destination_attribute)}], match: :full" + end + + size = + if attribute[:size] do + "size: #{attribute[:size]}" + end + + [ + "add #{inspect(attribute.source)}", + "references(:#{as_atom(table)}", + [ + "column: #{inspect(reference_attribute)}", + with_match, + "name: #{inspect(reference.name)}", + "type: #{inspect(reference_type(attribute, reference))}", + on_delete(reference), + on_update(reference), + size + ], + ")", + maybe_add_default(attribute.default), + maybe_add_primary_key(attribute.primary_key?), + maybe_add_null(attribute.allow_nil?) + ] + |> join() + end + + def up(%{ + attribute: + %{ + references: + %{ + table: table, + destination_attribute: destination_attribute + } = reference + } = attribute + }) do + size = + if attribute[:size] do + "size: #{attribute[:size]}" + end + + [ + "add #{inspect(attribute.source)}", + "references(:#{as_atom(table)}", + [ + "column: #{inspect(destination_attribute)}", + "name: #{inspect(reference.name)}", + "type: #{inspect(reference_type(attribute, reference))}", + size, + on_delete(reference), + on_update(reference) + ], + ")", + maybe_add_default(attribute.default), + maybe_add_primary_key(attribute.primary_key?), + maybe_add_null(attribute.allow_nil?) + ] + |> join() + end + + def up(%{attribute: %{type: :bigint, default: "nil", generated?: true} = attribute}) do + [ + "add #{inspect(attribute.source)}", + ":bigserial", + maybe_add_null(attribute.allow_nil?), + maybe_add_primary_key(attribute.primary_key?) + ] + |> join() + end + + def up(%{attribute: %{type: :integer, default: "nil", generated?: true} = attribute}) do + [ + "add #{inspect(attribute.source)}", + ":serial", + maybe_add_null(attribute.allow_nil?), + maybe_add_primary_key(attribute.primary_key?) + ] + |> join() + end + + def up(%{attribute: attribute}) do + size = + if attribute[:size] do + "size: #{attribute[:size]}" + end + + [ + "add #{inspect(attribute.source)}", + "#{inspect(attribute.type)}", + maybe_add_null(attribute.allow_nil?), + maybe_add_default(attribute.default), + size, + maybe_add_primary_key(attribute.primary_key?) + ] + |> join() + end + + def down( + %{ + attribute: attribute, + table: table, + multitenancy: multitenancy + } = op + ) do + AshSqlite.MigrationGenerator.Operation.RemoveAttribute.up(%{ + op + | attribute: attribute, + table: table, + multitenancy: multitenancy + }) + end + end + + defmodule AlterDeferrability do + @moduledoc false + defstruct [:table, :references, :direction, no_phase: true] + + def up(%{direction: :up, table: table, references: %{name: name, deferrable: true}}) do + "execute(\"ALTER TABLE #{table} alter CONSTRAINT #{name} DEFERRABLE INITIALLY IMMEDIATE\");" + end + + def up(%{direction: :up, table: table, references: %{name: name, deferrable: :initially}}) do + "execute(\"ALTER TABLE #{table} alter CONSTRAINT #{name} DEFERRABLE INITIALLY DEFERRED\");" + end + + def up(%{direction: :up, table: table, references: %{name: name}}) do + "execute(\"ALTER TABLE #{table} alter CONSTRAINT #{name} NOT DEFERRABLE\");" + end + + def up(_), do: "" + + def down(%{direction: :down} = data), do: up(%{data | direction: :up}) + def down(_), do: "" + end + + defmodule AlterAttribute do + @moduledoc false + defstruct [ + :old_attribute, + :new_attribute, + :table, + :multitenancy, + :old_multitenancy + ] + + import Helper + + defp alter_opts(attribute, old_attribute) do + primary_key = + cond do + attribute.primary_key? and !old_attribute.primary_key? -> + ", primary_key: true" + + old_attribute.primary_key? and !attribute.primary_key? -> + ", primary_key: false" + + true -> + nil + end + + default = + if attribute.default != old_attribute.default do + if is_nil(attribute.default) do + ", default: nil" + else + ", default: #{attribute.default}" + end + end + + null = + if attribute.allow_nil? != old_attribute.allow_nil? do + ", null: #{attribute.allow_nil?}" + end + + "#{null}#{default}#{primary_key}" + end + + def up(%{ + multitenancy: multitenancy, + old_attribute: old_attribute, + new_attribute: attribute + }) do + type_or_reference = + if AshSqlite.MigrationGenerator.has_reference?(multitenancy, attribute) and + Map.get(old_attribute, :references) != Map.get(attribute, :references) do + reference(multitenancy, attribute) + else + inspect(attribute.type) + end + + "modify #{inspect(attribute.source)}, #{type_or_reference}#{alter_opts(attribute, old_attribute)}" + end + + defp reference( + %{strategy: :attribute, attribute: source_attribute}, + %{ + references: + %{ + multitenancy: %{strategy: :attribute, attribute: destination_attribute}, + table: table, + destination_attribute: reference_attribute + } = reference + } = attribute + ) do + with_match = + if destination_attribute != reference_attribute do + "with: [#{as_atom(source_attribute)}: :#{as_atom(destination_attribute)}], match: :full" + end + + size = + if attribute[:size] do + "size: #{attribute[:size]}" + end + + join([ + "references(:#{as_atom(table)}, column: #{inspect(reference_attribute)}", + with_match, + "name: #{inspect(reference.name)}", + "type: #{inspect(reference_type(attribute, reference))}", + size, + on_delete(reference), + on_update(reference), + ")" + ]) + end + + defp reference( + _, + %{ + references: + %{ + table: table, + destination_attribute: destination_attribute + } = reference + } = attribute + ) do + size = + if attribute[:size] do + "size: #{attribute[:size]}" + end + + join([ + "references(:#{as_atom(table)}, column: #{inspect(destination_attribute)}", + "name: #{inspect(reference.name)}", + "type: #{inspect(reference_type(attribute, reference))}", + size, + on_delete(reference), + on_update(reference), + ")" + ]) + end + + def down(op) do + up(%{ + op + | old_attribute: op.new_attribute, + new_attribute: op.old_attribute, + old_multitenancy: op.multitenancy, + multitenancy: op.old_multitenancy + }) + end + end + + defmodule DropForeignKey do + @moduledoc false + # We only run this migration in one direction, based on the input + # This is because the creation of a foreign key is handled by `references/3` + # We only need to drop it before altering an attribute with `references/3` + defstruct [:attribute, :table, :multitenancy, :direction, no_phase: true] + + import Helper + + def up(%{table: table, attribute: %{references: reference}, direction: :up}) do + "drop constraint(:#{as_atom(table)}, #{join([inspect(reference.name)])})" + end + + def up(_) do + "" + end + + def down(%{ + table: table, + attribute: %{references: reference}, + direction: :down + }) do + "drop constraint(:#{as_atom(table)}, #{join([inspect(reference.name)])})" + end + + def down(_) do + "" + end + end + + defmodule RenameAttribute do + @moduledoc false + defstruct [ + :old_attribute, + :new_attribute, + :table, + :multitenancy, + :old_multitenancy, + no_phase: true + ] + + import Helper + + def up(%{ + old_attribute: old_attribute, + new_attribute: new_attribute, + table: table + }) do + table_statement = join([":#{as_atom(table)}"]) + + "rename table(#{table_statement}), #{inspect(old_attribute.source)}, to: #{inspect(new_attribute.source)}" + end + + def down( + %{ + old_attribute: old_attribute, + new_attribute: new_attribute + } = data + ) do + up(%{data | new_attribute: old_attribute, old_attribute: new_attribute}) + end + end + + defmodule RemoveAttribute do + @moduledoc false + defstruct [:attribute, :table, :multitenancy, :old_multitenancy, commented?: true] + + def up(%{attribute: attribute, commented?: true}) do + """ + # Attribute removal has been commented out to avoid data loss. See the migration generator documentation for more + # If you uncomment this, be sure to also uncomment the corresponding attribute *addition* in the `down` migration + # remove #{inspect(attribute.source)} + """ + end + + def up(%{attribute: attribute}) do + "remove #{inspect(attribute.source)}" + end + + def down(%{attribute: attribute, multitenancy: multitenancy, commented?: true}) do + prefix = """ + # This is the `down` migration of the statement: + # + # remove #{inspect(attribute.source)} + # + """ + + contents = + %AshSqlite.MigrationGenerator.Operation.AddAttribute{ + attribute: attribute, + multitenancy: multitenancy + } + |> AshSqlite.MigrationGenerator.Operation.AddAttribute.up() + |> String.split("\n") + |> Enum.map_join("\n", &"# #{&1}") + + prefix <> "\n" <> contents + end + + def down(%{attribute: attribute, multitenancy: multitenancy, table: table}) do + AshSqlite.MigrationGenerator.Operation.AddAttribute.up( + %AshSqlite.MigrationGenerator.Operation.AddAttribute{ + attribute: attribute, + table: table, + multitenancy: multitenancy + } + ) + end + end + + defmodule AddUniqueIndex do + @moduledoc false + defstruct [:identity, :table, :multitenancy, :old_multitenancy, no_phase: true] + + import Helper + + def up(%{ + identity: %{name: name, keys: keys, base_filter: base_filter, index_name: index_name}, + table: table, + multitenancy: multitenancy + }) do + keys = + case multitenancy.strategy do + :attribute -> + [multitenancy.attribute | keys] + + _ -> + keys + end + + index_name = index_name || "#{table}_#{name}_index" + + if base_filter do + "create unique_index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], where: \"#{base_filter}\", #{join(["name: \"#{index_name}\""])})" + else + "create unique_index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{join(["name: \"#{index_name}\""])})" + end + end + + def down(%{ + identity: %{name: name, keys: keys, index_name: index_name}, + table: table, + multitenancy: multitenancy + }) do + keys = + case multitenancy.strategy do + :attribute -> + [multitenancy.attribute | keys] + + _ -> + keys + end + + index_name = index_name || "#{table}_#{name}_index" + + "drop_if_exists unique_index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{join(["name: \"#{index_name}\""])})" + end + end + + defmodule AddCustomStatement do + @moduledoc false + defstruct [:statement, :table, no_phase: true] + + def up(%{statement: %{up: up, code?: false}}) do + """ + execute(\"\"\" + #{String.trim(up)} + \"\"\") + """ + end + + def up(%{statement: %{up: up, code?: true}}) do + up + end + + def down(%{statement: %{down: down, code?: false}}) do + """ + execute(\"\"\" + #{String.trim(down)} + \"\"\") + """ + end + + def down(%{statement: %{down: down, code?: true}}) do + down + end + end + + defmodule RemoveCustomStatement do + @moduledoc false + defstruct [:statement, :table, no_phase: true] + + def up(%{statement: statement, table: table}) do + AddCustomStatement.down(%AddCustomStatement{statement: statement, table: table}) + end + + def down(%{statement: statement, table: table}) do + AddCustomStatement.up(%AddCustomStatement{statement: statement, table: table}) + end + end + + defmodule AddCustomIndex do + @moduledoc false + defstruct [:table, :index, :base_filter, :multitenancy, no_phase: true] + import Helper + + def up(%{ + index: index, + table: table, + base_filter: base_filter, + multitenancy: multitenancy + }) do + keys = + case multitenancy.strategy do + :attribute -> + [to_string(multitenancy.attribute) | Enum.map(index.fields, &to_string/1)] + + _ -> + Enum.map(index.fields, &to_string/1) + end + + index = + if index.where && base_filter do + %{index | where: base_filter <> " AND " <> index.where} + else + index + end + + opts = + join([ + option(:name, index.name), + option(:unique, index.unique), + option(:using, index.using), + option(:where, index.where), + option(:include, index.include) + ]) + + if opts == "", + do: "create index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}])", + else: + "create index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{opts})" + end + + def down(%{index: index, table: table, multitenancy: multitenancy}) do + index_name = AshSqlite.CustomIndex.name(table, index) + + keys = + case multitenancy.strategy do + :attribute -> + [to_string(multitenancy.attribute) | Enum.map(index.fields, &to_string/1)] + + _ -> + Enum.map(index.fields, &to_string/1) + end + + "drop_if_exists index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{join(["name: \"#{index_name}\""])})" + end + end + + defmodule RemovePrimaryKey do + @moduledoc false + defstruct [:table, no_phase: true] + + def up(%{table: table}) do + "drop constraint(#{inspect(table)}, \"#{table}_pkey\")" + end + + def down(_) do + "" + end + end + + defmodule RemovePrimaryKeyDown do + @moduledoc false + defstruct [:table, no_phase: true] + + def up(_) do + "" + end + + def down(%{table: table}) do + "drop constraint(#{inspect(table)}, \"#{table}_pkey\")" + end + end + + defmodule RemoveCustomIndex do + @moduledoc false + defstruct [:table, :index, :base_filter, :multitenancy, no_phase: true] + import Helper + + def up(%{index: index, table: table, multitenancy: multitenancy}) do + index_name = AshSqlite.CustomIndex.name(table, index) + + keys = + case multitenancy.strategy do + :attribute -> + [to_string(multitenancy.attribute) | Enum.map(index.fields, &to_string/1)] + + _ -> + Enum.map(index.fields, &to_string/1) + end + + "drop_if_exists index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{join(["name: \"#{index_name}\""])})" + end + + def down(%{ + index: index, + table: table, + base_filter: base_filter, + multitenancy: multitenancy + }) do + keys = + case multitenancy.strategy do + :attribute -> + [to_string(multitenancy.attribute) | Enum.map(index.fields, &to_string/1)] + + _ -> + Enum.map(index.fields, &to_string/1) + end + + index = + if index.where && base_filter do + %{index | where: base_filter <> " AND " <> index.where} + else + index + end + + opts = + join([ + option(:name, index.name), + option(:unique, index.unique), + option(:using, index.using), + option(:where, index.where), + option(:include, index.include) + ]) + + if opts == "" do + "create index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}])" + else + "create index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{opts})" + end + end + end + + defmodule RenameUniqueIndex do + @moduledoc false + defstruct [ + :new_identity, + :old_identity, + :table, + :multitenancy, + :old_multitenancy, + no_phase: true + ] + + def up(%{ + old_identity: %{index_name: old_index_name, name: old_name}, + new_identity: %{index_name: new_index_name}, + table: table + }) do + old_index_name = old_index_name || "#{table}_#{old_name}_index" + + "execute(\"ALTER INDEX #{old_index_name} " <> + "RENAME TO #{new_index_name}\")\n" + end + + def down(%{ + old_identity: %{index_name: old_index_name, name: old_name}, + new_identity: %{index_name: new_index_name}, + table: table + }) do + old_index_name = old_index_name || "#{table}_#{old_name}_index" + + "execute(\"ALTER INDEX #{new_index_name} " <> + "RENAME TO #{old_index_name}\")\n" + end + end + + defmodule RemoveUniqueIndex do + @moduledoc false + defstruct [:identity, :table, :multitenancy, :old_multitenancy, no_phase: true] + + import Helper + + def up(%{ + identity: %{name: name, keys: keys, index_name: index_name}, + table: table, + old_multitenancy: multitenancy + }) do + keys = + case multitenancy.strategy do + :attribute -> + [multitenancy.attribute | keys] + + _ -> + keys + end + + index_name = index_name || "#{table}_#{name}_index" + + "drop_if_exists unique_index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{join(["name: \"#{index_name}\""])})" + end + + def down(%{ + identity: %{name: name, keys: keys, base_filter: base_filter, index_name: index_name}, + table: table, + multitenancy: multitenancy + }) do + keys = + case multitenancy.strategy do + :attribute -> + [multitenancy.attribute | keys] + + _ -> + keys + end + + index_name = index_name || "#{table}_#{name}_index" + + if base_filter do + "create unique_index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], where: \"#{base_filter}\", #{join(["name: \"#{index_name}\""])})" + else + "create unique_index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{join(["name: \"#{index_name}\""])})" + end + end + end +end diff --git a/lib/migration_generator/phase.ex b/lib/migration_generator/phase.ex new file mode 100644 index 0000000..1ed4f3e --- /dev/null +++ b/lib/migration_generator/phase.ex @@ -0,0 +1,66 @@ +defmodule AshSqlite.MigrationGenerator.Phase do + @moduledoc false + + defmodule Create do + @moduledoc false + defstruct [:table, :multitenancy, operations: [], commented?: false] + + import AshSqlite.MigrationGenerator.Operation.Helper, only: [as_atom: 1] + + def up(%{table: table, operations: operations}) do + opts = "" + + "create table(:#{as_atom(table)}, primary_key: false#{opts}) do\n" <> + Enum.map_join(operations, "\n", fn operation -> operation.__struct__.up(operation) end) <> + "\nend" + end + + def down(%{table: table}) do + opts = "" + + "drop table(:#{as_atom(table)}#{opts})" + end + end + + defmodule Alter do + @moduledoc false + defstruct [:table, :multitenancy, operations: [], commented?: false] + + import AshSqlite.MigrationGenerator.Operation.Helper, only: [as_atom: 1] + + def up(%{table: table, operations: operations}) do + body = + operations + |> Enum.map_join("\n", fn operation -> operation.__struct__.up(operation) end) + |> String.trim() + + if body == "" do + "" + else + opts = "" + + "alter table(:#{as_atom(table)}#{opts}) do\n" <> + body <> + "\nend" + end + end + + def down(%{table: table, operations: operations}) do + body = + operations + |> Enum.reverse() + |> Enum.map_join("\n", fn operation -> operation.__struct__.down(operation) end) + |> String.trim() + + if body == "" do + "" + else + opts = "" + + "alter table(:#{as_atom(table)}#{opts}) do\n" <> + body <> + "\nend" + end + end + end +end diff --git a/lib/mix/helpers.ex b/lib/mix/helpers.ex new file mode 100644 index 0000000..8b8470f --- /dev/null +++ b/lib/mix/helpers.ex @@ -0,0 +1,132 @@ +defmodule AshSqlite.Mix.Helpers do + @moduledoc false + def domains!(opts, args) do + apps = + if apps_paths = Mix.Project.apps_paths() do + apps_paths |> Map.keys() |> Enum.sort() + else + [Mix.Project.config()[:app]] + end + + configured_domains = Enum.flat_map(apps, &Application.get_env(&1, :ash_domains, [])) + + domains = + if opts[:domains] && opts[:domains] != "" do + opts[:domains] + |> Kernel.||("") + |> String.split(",") + |> Enum.flat_map(fn + "" -> + [] + + domain -> + [Module.concat([domain])] + end) + else + configured_domains + end + + domains + |> Enum.map(&ensure_compiled(&1, args)) + |> case do + [] -> + raise "must supply the --domains argument, or set `config :my_app, ash_domains: [...]` in config" + + domains -> + domains + end + end + + def repos!(opts, args) do + domains = domains!(opts, args) + + resources = + domains + |> Enum.flat_map(&Ash.Domain.Info.resources/1) + |> Enum.filter(&(Ash.DataLayer.data_layer(&1) == AshSqlite.DataLayer)) + |> case do + [] -> + raise """ + No resources with `data_layer: AshSqlite.DataLayer` found in the domains #{Enum.map_join(domains, ",", &inspect/1)}. + + Must be able to find at least one resource with `data_layer: AshSqlite.DataLayer`. + """ + + resources -> + resources + end + + resources + |> Enum.map(&AshSqlite.DataLayer.Info.repo(&1)) + |> Enum.uniq() + |> case do + [] -> + raise """ + No repos could be found configured on the resources in the domains: #{Enum.map_join(domains, ",", &inspect/1)} + + At least one resource must have a repo configured. + + The following resources were found with `data_layer: AshSqlite.DataLayer`: + + #{Enum.map_join(resources, "\n", &"* #{inspect(&1)}")} + """ + + repos -> + repos + end + end + + def delete_flag(args, arg) do + case Enum.split_while(args, &(&1 != arg)) do + {left, [_ | rest]} -> + left ++ rest + + _ -> + args + end + end + + def delete_arg(args, arg) do + case Enum.split_while(args, &(&1 != arg)) do + {left, [_, _ | rest]} -> + left ++ rest + + _ -> + args + end + end + + defp ensure_compiled(domain, args) do + if Code.ensure_loaded?(Mix.Tasks.App.Config) do + Mix.Task.run("app.config", args) + else + Mix.Task.run("loadpaths", args) + "--no-compile" not in args && Mix.Task.run("compile", args) + end + + case Code.ensure_compiled(domain) do + {:module, _} -> + domain + |> Ash.Domain.Info.resources() + |> Enum.each(&Code.ensure_compiled/1) + + # TODO: We shouldn't need to make sure that the resources are compiled + + domain + + {:error, error} -> + Mix.raise("Could not load #{inspect(domain)}, error: #{inspect(error)}. ") + end + end + + def migrations_path(opts, repo) do + opts[:migrations_path] || repo.config()[:migrations_path] || derive_migrations_path(repo) + end + + def derive_migrations_path(repo) do + config = repo.config() + priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" + app = Keyword.fetch!(config, :otp_app) + Application.app_dir(app, Path.join(priv, "migrations")) + end +end diff --git a/lib/mix/tasks/ash_sqlite.create.ex b/lib/mix/tasks/ash_sqlite.create.ex new file mode 100644 index 0000000..a5da23e --- /dev/null +++ b/lib/mix/tasks/ash_sqlite.create.ex @@ -0,0 +1,50 @@ +defmodule Mix.Tasks.AshSqlite.Create do + use Mix.Task + + @shortdoc "Creates the repository storage" + + @switches [ + quiet: :boolean, + domains: :string, + no_compile: :boolean, + no_deps_check: :boolean + ] + + @aliases [ + q: :quiet + ] + + @moduledoc """ + Create the storage for repos in all resources for the given (or configured) domains. + + ## Examples + + mix ash_sqlite.create + mix ash_sqlite.create --domains MyApp.Domain1,MyApp.Domain2 + + ## Command line options + + * `--domains` - the domains who's repos you want to migrate. + * `--quiet` - do not log output + * `--no-compile` - do not compile before creating + * `--no-deps-check` - do not compile before creating + """ + + @doc false + def run(args) do + {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) + + repos = AshSqlite.Mix.Helpers.repos!(opts, args) + + repo_args = + Enum.flat_map(repos, fn repo -> + ["-r", to_string(repo)] + end) + + rest_opts = AshSqlite.Mix.Helpers.delete_arg(args, "--domains") + + Mix.Task.reenable("ecto.create") + + Mix.Task.run("ecto.create", repo_args ++ rest_opts) + end +end diff --git a/lib/mix/tasks/ash_sqlite.drop.ex b/lib/mix/tasks/ash_sqlite.drop.ex new file mode 100644 index 0000000..6dbc968 --- /dev/null +++ b/lib/mix/tasks/ash_sqlite.drop.ex @@ -0,0 +1,58 @@ +defmodule Mix.Tasks.AshSqlite.Drop do + use Mix.Task + + @shortdoc "Drops the repository storage for the repos in the specified (or configured) domains" + @default_opts [force: false, force_drop: false] + + @aliases [ + f: :force, + q: :quiet + ] + + @switches [ + force: :boolean, + force_drop: :boolean, + quiet: :boolean, + domains: :string, + no_compile: :boolean, + no_deps_check: :boolean + ] + + @moduledoc """ + Drop the storage for the given repository. + + ## Examples + + mix ash_sqlite.drop + mix ash_sqlite.drop -r MyApp.Domain1,MyApp.Domain2 + + ## Command line options + + * `--doains` - the domains who's repos should be dropped + * `-q`, `--quiet` - run the command quietly + * `-f`, `--force` - do not ask for confirmation when dropping the database. + Configuration is asked only when `:start_permanent` is set to true + (typically in production) + * `--no-compile` - do not compile before dropping + * `--no-deps-check` - do not compile before dropping + """ + + @doc false + def run(args) do + {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) + opts = Keyword.merge(@default_opts, opts) + + repos = AshSqlite.Mix.Helpers.repos!(opts, args) + + repo_args = + Enum.flat_map(repos, fn repo -> + ["-r", to_string(repo)] + end) + + rest_opts = AshSqlite.Mix.Helpers.delete_arg(args, "--domains") + + Mix.Task.reenable("ecto.drop") + + Mix.Task.run("ecto.drop", repo_args ++ rest_opts) + end +end diff --git a/lib/mix/tasks/ash_sqlite.generate_migrations.ex b/lib/mix/tasks/ash_sqlite.generate_migrations.ex new file mode 100644 index 0000000..02d70d8 --- /dev/null +++ b/lib/mix/tasks/ash_sqlite.generate_migrations.ex @@ -0,0 +1,95 @@ +defmodule Mix.Tasks.AshSqlite.GenerateMigrations do + @moduledoc """ + Generates migrations, and stores a snapshot of your resources. + + Options: + + * `domains` - a comma separated list of domain modules, for which migrations will be generated + * `snapshot-path` - a custom path to store the snapshots, defaults to "priv/resource_snapshots" + * `migration-path` - a custom path to store the migrations, defaults to "priv". + Migrations are stored in a folder for each repo, so `priv/repo_name/migrations` + * `drop-columns` - whether or not to drop columns as attributes are removed. See below for more + * `name` - + names the generated migrations, prepending with the timestamp. The default is `migrate_resources_`, + where `` is the count of migrations matching `*migrate_resources*` plus one. + For example, `--name add_special_column` would get a name like `20210708181402_add_special_column.exs` + + Flags: + + * `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` - no files are created, returns an exit(1) code if the current snapshots and resources don't fit + + #### Snapshots + + Snapshots are stored in a folder for each table that migrations are generated for. Each snapshot is + stored in a file with a timestamp of when it was generated. + This is important because it allows for simultaneous work to be done on separate branches, and for rolling back + changes more easily, e.g removing a generated migration, and deleting the most recent snapshot, without having to redo + all of it + + #### Dropping columns + + Generally speaking, it is bad practice to drop columns when you deploy a change that + would remove an attribute. The main reasons for this are backwards compatibility and rolling restarts. + If you deploy an attribute removal, and run migrations. Regardless of your deployment sstrategy, you + won't be able to roll back, because the data has been deleted. In a rolling restart situation, some of + the machines/pods/whatever may still be running after the column has been deleted, causing errors. With + this in mind, its best not to delete those columns until later, after the data has been confirmed unnecessary. + To that end, the migration generator leaves the column dropping code commented. You can pass `--drop_columns` + to tell it to uncomment those statements. Additionally, you can just uncomment that code on a case by case + basis. + + #### Conflicts/Multiple Resources + + It will raise on conflicts that it can't resolve, like the same field with different + types. It will prompt to resolve conflicts that can be resolved with human input. + For example, if you remove an attribute and add an attribute, it will ask you if you are renaming + the column in question. If not, it will remove one column and add the other. + + Additionally, it lowers things to the database where possible: + + #### Defaults + There are three anonymous functions that will translate to database-specific defaults currently: + + * `&DateTime.utc_now/0` + + Non-function default values will be dumped to their native type and inspected. This may not work for some types, + and may require manual intervention/patches to the migration generator code. + + #### Identities + + Identities will cause the migration generator to generate unique constraints. If multiple + resources target the same table, you will be asked to select the primary key, and any others + will be added as unique constraints. + """ + use Mix.Task + + @shortdoc "Generates migrations, and stores a snapshot of your resources" + def run(args) do + {opts, _} = + OptionParser.parse!(args, + strict: [ + domains: :string, + snapshot_path: :string, + migration_path: :string, + quiet: :boolean, + name: :string, + no_format: :boolean, + dry_run: :boolean, + check: :boolean, + drop_columns: :boolean + ] + ) + + domains = AshSqlite.Mix.Helpers.domains!(opts, args) + + opts = + opts + |> Keyword.put(:format, !opts[:no_format]) + |> Keyword.delete(:no_format) + + AshSqlite.MigrationGenerator.generate(domains, opts) + end +end diff --git a/lib/mix/tasks/ash_sqlite.migrate.ex b/lib/mix/tasks/ash_sqlite.migrate.ex new file mode 100644 index 0000000..8e4f058 --- /dev/null +++ b/lib/mix/tasks/ash_sqlite.migrate.ex @@ -0,0 +1,116 @@ +defmodule Mix.Tasks.AshSqlite.Migrate do + use Mix.Task + + import AshSqlite.Mix.Helpers, + only: [migrations_path: 2] + + @shortdoc "Runs the repository migrations for all repositories in the provided (or congigured) domains" + + @aliases [ + n: :step + ] + + @switches [ + all: :boolean, + step: :integer, + to: :integer, + quiet: :boolean, + pool_size: :integer, + log_sql: :boolean, + strict_version_order: :boolean, + domains: :string, + no_compile: :boolean, + no_deps_check: :boolean, + migrations_path: :keep + ] + + @moduledoc """ + Runs the pending migrations for the given repository. + + Migrations are expected at "priv/YOUR_REPO/migrations" directory + of the current application, where "YOUR_REPO" is the last segment + in your repository name. For example, the repository `MyApp.Repo` + will use "priv/repo/migrations". The repository `Whatever.MyRepo` + will use "priv/my_repo/migrations". + + This task runs all pending migrations by default. To migrate up to a + specific version number, supply `--to version_number`. To migrate a + specific number of times, use `--step n`. + + This is only really useful if your domain or domains only use a single repo. + If you have multiple repos and you want to run a single migration and/or + migrate/roll them back to different points, you will need to use the + ecto specific task, `mix ecto.migrate` and provide your repo name. + + If a repository has not yet been started, one will be started outside + your application supervision tree and shutdown afterwards. + + ## Examples + + mix ash_sqlite.migrate + mix ash_sqlite.migrate --domains MyApp.Domain1,MyApp.Domain2 + + mix ash_sqlite.migrate -n 3 + mix ash_sqlite.migrate --step 3 + + mix ash_sqlite.migrate --to 20080906120000 + + ## Command line options + + * `--domains` - the domains who's repos should be migrated + + * `--all` - run all pending migrations + + * `--step`, `-n` - run n number of pending migrations + + * `--to` - run all migrations up to and including version + + * `--quiet` - do not log migration commands + + * `--pool-size` - the pool size if the repository is started only for the task (defaults to 2) + + * `--log-sql` - log the raw sql migrations are running + + * `--strict-version-order` - abort when applying a migration with old timestamp + + * `--no-compile` - does not compile applications before migrating + + * `--no-deps-check` - does not check depedendencies before migrating + + * `--migrations-path` - the path to load the migrations from, defaults to + `"priv/repo/migrations"`. This option may be given multiple times in which case the migrations + are loaded from all the given directories and sorted as if they were in the same one. + + Note, if you have migrations paths e.g. `a/` and `b/`, and run + `mix ecto.migrate --migrations-path a/`, the latest migrations from `a/` will be run (even + if `b/` contains the overall latest migrations.) + """ + + @impl true + def run(args) do + {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) + + repos = AshSqlite.Mix.Helpers.repos!(opts, args) + + repo_args = + Enum.flat_map(repos, fn repo -> + ["-r", to_string(repo)] + end) + + rest_opts = + args + |> AshSqlite.Mix.Helpers.delete_arg("--domains") + |> AshSqlite.Mix.Helpers.delete_arg("--migrations-path") + + Mix.Task.reenable("ecto.migrate") + + for repo <- repos do + Mix.Task.run( + "ecto.migrate", + repo_args ++ rest_opts ++ ["--migrations-path", migrations_path(opts, repo)] + ) + + Mix.Task.reenable("ecto.migrate") + end + end +end diff --git a/lib/mix/tasks/ash_sqlite.rollback.ex b/lib/mix/tasks/ash_sqlite.rollback.ex new file mode 100644 index 0000000..0ae4c19 --- /dev/null +++ b/lib/mix/tasks/ash_sqlite.rollback.ex @@ -0,0 +1,81 @@ +defmodule Mix.Tasks.AshSqlite.Rollback do + use Mix.Task + + import AshSqlite.Mix.Helpers, + only: [migrations_path: 2] + + @shortdoc "Rolls back the repository migrations for all repositories in the provided (or configured) domains" + + @moduledoc """ + Reverts applied migrations in the given repository. + Migrations are expected at "priv/YOUR_REPO/migrations" directory + of the current application but it can be configured by specifying + the `:priv` key under the repository configuration. + Runs the latest applied migration by default. To roll back to + a version number, supply `--to version_number`. To roll back a + specific number of times, use `--step n`. To undo all applied + migrations, provide `--all`. + + This is only really useful if your domain or domains only use a single repo. + If you have multiple repos and you want to run a single migration and/or + migrate/roll them back to different points, you will need to use the + ecto specific task, `mix ecto.migrate` and provide your repo name. + + ## Examples + mix ash_sqlite.rollback + mix ash_sqlite.rollback -r Custom.Repo + mix ash_sqlite.rollback -n 3 + mix ash_sqlite.rollback --step 3 + mix ash_sqlite.rollback -v 20080906120000 + mix ash_sqlite.rollback --to 20080906120000 + + ## Command line options + * `--domains` - the domains who's repos should be rolledback + * `--all` - revert all applied migrations + * `--step` / `-n` - revert n number of applied migrations + * `--to` / `-v` - revert all migrations down to and including version + * `--quiet` - do not log migration commands + * `--pool-size` - the pool size if the repository is started only for the task (defaults to 1) + * `--log-sql` - log the raw sql migrations are running + """ + + @doc false + def run(args) do + {opts, _, _} = + OptionParser.parse(args, + switches: [ + all: :boolean, + step: :integer, + to: :integer, + start: :boolean, + quiet: :boolean, + pool_size: :integer, + log_sql: :boolean + ], + aliases: [n: :step, v: :to] + ) + + repos = AshSqlite.Mix.Helpers.repos!(opts, args) + + repo_args = + Enum.flat_map(repos, fn repo -> + ["-r", to_string(repo)] + end) + + rest_opts = + args + |> AshSqlite.Mix.Helpers.delete_arg("--domains") + |> AshSqlite.Mix.Helpers.delete_arg("--migrations-path") + + Mix.Task.reenable("ecto.rollback") + + for repo <- repos do + Mix.Task.run( + "ecto.rollback", + repo_args ++ rest_opts ++ ["--migrations-path", migrations_path(opts, repo)] + ) + + Mix.Task.reenable("ecto.rollback") + end + end +end diff --git a/lib/reference.ex b/lib/reference.ex new file mode 100644 index 0000000..275bfd7 --- /dev/null +++ b/lib/reference.ex @@ -0,0 +1,43 @@ +defmodule AshSqlite.Reference do + @moduledoc "Represents the configuration of a reference (i.e foreign key)." + defstruct [:relationship, :on_delete, :on_update, :name, :deferrable, ignore?: false] + + def schema do + [ + relationship: [ + type: :atom, + required: true, + doc: "The relationship to be configured" + ], + ignore?: [ + type: :boolean, + doc: + "If set to true, no reference is created for the given relationship. This is useful if you need to define it in some custom way" + ], + on_delete: [ + type: {:one_of, [:delete, :nilify, :nothing, :restrict]}, + doc: """ + What should happen to records of this resource when the referenced record of the *destination* resource is deleted. + """ + ], + on_update: [ + type: {:one_of, [:update, :nilify, :nothing, :restrict]}, + doc: """ + What should happen to records of this resource when the referenced destination_attribute of the *destination* record is update. + """ + ], + deferrable: [ + type: {:one_of, [false, true, :initially]}, + default: false, + doc: """ + Wether or not the constraint is deferrable. This only affects the migration generator. + """ + ], + name: [ + type: :string, + doc: + "The name of the foreign key to generate in the database. Defaults to
__fkey" + ] + ] + end +end diff --git a/lib/repo.ex b/lib/repo.ex new file mode 100644 index 0000000..2bed9af --- /dev/null +++ b/lib/repo.ex @@ -0,0 +1,155 @@ +defmodule AshSqlite.Repo do + @moduledoc """ + Resources that use `AshSqlite.DataLayer` use a `Repo` to access the database. + + This repo is a thin wrapper around an `Ecto.Repo`. + + You can use `Ecto.Repo`'s `init/2` to configure your repo like normal, but + instead of returning `{:ok, config}`, use `super(config)` to pass the + configuration to the `AshSqlite.Repo` implementation. + """ + + @doc "Use this to inform the data layer about what extensions are installed" + @callback installed_extensions() :: [String.t()] + + @doc """ + Use this to inform the data layer about the oldest potential sqlite version it will be run on. + + Must be an integer greater than or equal to 13. + """ + @callback min_pg_version() :: integer() + + @doc "The path where your migrations are stored" + @callback migrations_path() :: String.t() | nil + @doc "Allows overriding a given migration type for *all* fields, for example if you wanted to always use :timestamptz for :utc_datetime fields" + @callback override_migration_type(atom) :: atom + + defmacro __using__(opts) do + quote bind_quoted: [opts: opts] do + otp_app = opts[:otp_app] || raise("Must configure OTP app") + + use Ecto.Repo, + adapter: Ecto.Adapters.MyXQL, + otp_app: otp_app + + @behaviour AshSqlite.Repo + + defoverridable insert: 2, insert: 1, insert!: 2, insert!: 1 + + def installed_extensions, do: [] + def migrations_path, do: nil + def override_migration_type(type), do: type + def min_pg_version, do: 10 + + def init(_, config) do + new_config = + config + |> Keyword.put(:installed_extensions, installed_extensions()) + |> Keyword.put(:migrations_path, migrations_path()) + |> Keyword.put(:case_sensitive_like, :on) + + {:ok, new_config} + end + + def insert(struct_or_changeset, opts \\ []) do + struct_or_changeset + |> to_ecto() + |> then(fn value -> + repo = get_dynamic_repo() + + Ecto.Repo.Schema.insert( + __MODULE__, + repo, + value, + Ecto.Repo.Supervisor.tuplet(repo, prepare_opts(:insert, opts)) + ) + end) + |> from_ecto() + end + + def insert!(struct_or_changeset, opts \\ []) do + struct_or_changeset + |> to_ecto() + |> then(fn value -> + repo = get_dynamic_repo() + + Ecto.Repo.Schema.insert!( + __MODULE__, + repo, + value, + Ecto.Repo.Supervisor.tuplet(repo, prepare_opts(:insert, opts)) + ) + end) + |> from_ecto() + end + + def from_ecto({:ok, result}), do: {:ok, from_ecto(result)} + def from_ecto({:error, _} = other), do: other + + def from_ecto(nil), do: nil + + def from_ecto(value) when is_list(value) do + Enum.map(value, &from_ecto/1) + end + + def from_ecto(%resource{} = record) do + if Spark.Dsl.is?(resource, Ash.Resource) do + empty = struct(resource) + + resource + |> Ash.Resource.Info.relationships() + |> Enum.reduce(record, fn relationship, record -> + case Map.get(record, relationship.name) do + %Ecto.Association.NotLoaded{} -> + Map.put(record, relationship.name, Map.get(empty, relationship.name)) + + value -> + Map.put(record, relationship.name, from_ecto(value)) + end + end) + else + record + end + end + + def from_ecto(other), do: other + + def to_ecto(nil), do: nil + + def to_ecto(value) when is_list(value) do + Enum.map(value, &to_ecto/1) + end + + def to_ecto(%resource{} = record) do + if Spark.Dsl.is?(resource, Ash.Resource) do + resource + |> Ash.Resource.Info.relationships() + |> Enum.reduce(record, fn relationship, record -> + value = + case Map.get(record, relationship.name) do + %Ash.NotLoaded{} -> + %Ecto.Association.NotLoaded{ + __field__: relationship.name, + __cardinality__: relationship.cardinality + } + + value -> + to_ecto(value) + end + + Map.put(record, relationship.name, value) + end) + else + record + end + end + + def to_ecto(other), do: other + + defoverridable init: 2, + installed_extensions: 0, + override_migration_type: 1, + min_pg_version: 0 + end + end +end diff --git a/lib/sql_implementation.ex b/lib/sql_implementation.ex new file mode 100644 index 0000000..509890e --- /dev/null +++ b/lib/sql_implementation.ex @@ -0,0 +1,449 @@ +defmodule AshSqlite.SqlImplementation do + @moduledoc false + use AshSql.Implementation + + require Ecto.Query + + @impl true + def manual_relationship_function, do: :ash_sqlite_join + + @impl true + def manual_relationship_subquery_function, do: :ash_sqlite_subquery + + @impl true + def strpos_function, do: "instr" + + @impl true + def ilike?, do: false + + @impl true + def expr( + query, + %like{arguments: [arg1, arg2], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + type + ) + when like in [AshSqlite.Functions.Like, AshSqlite.Functions.ILike] do + {arg1, acc} = + AshSql.Expr.dynamic_expr(query, arg1, bindings, pred_embedded? || embedded?, :string, acc) + + {arg2, acc} = + AshSql.Expr.dynamic_expr(query, arg2, bindings, pred_embedded? || embedded?, :string, acc) + + inner_dyn = + if like == AshSqlite.Functions.Like do + Ecto.Query.dynamic(like(^arg1, ^arg2)) + else + Ecto.Query.dynamic(like(fragment("LOWER(?)", ^arg1), fragment("LOWER(?)", ^arg2))) + end + + if type != Ash.Type.Boolean do + {:ok, inner_dyn, acc} + else + {:ok, Ecto.Query.dynamic(type(^inner_dyn, ^type)), acc} + end + end + + def expr( + query, + %Ash.Query.Function.GetPath{ + arguments: [%Ash.Query.Ref{attribute: %{type: type}}, right] + } = get_path, + bindings, + embedded?, + acc, + nil + ) + when is_atom(type) and is_list(right) do + if Ash.Type.embedded_type?(type) do + type = determine_type_at_path(type, right) + + do_get_path(query, get_path, bindings, embedded?, acc, type) + else + do_get_path(query, get_path, bindings, embedded?, acc) + end + end + + def expr( + query, + %Ash.Query.Function.GetPath{ + arguments: [%Ash.Query.Ref{attribute: %{type: {:array, type}}}, right] + } = get_path, + bindings, + embedded?, + acc, + nil + ) + when is_atom(type) and is_list(right) do + if Ash.Type.embedded_type?(type) do + type = determine_type_at_path(type, right) + do_get_path(query, get_path, bindings, embedded?, acc, type) + else + do_get_path(query, get_path, bindings, embedded?, acc) + end + end + + def expr( + query, + %Ash.Query.Function.GetPath{} = get_path, + bindings, + embedded?, + acc, + type + ) do + do_get_path(query, get_path, bindings, embedded?, acc, type) + end + + @impl true + def expr( + _query, + _expr, + _bindings, + _embedded?, + _acc, + _type + ) do + :error + end + + @impl true + def type_expr(expr, nil), do: expr + + def type_expr(expr, type) when is_atom(type) do + type = Ash.Type.get_type(type) + + cond do + !Ash.Type.ash_type?(type) -> + Ecto.Query.dynamic(type(^expr, ^type)) + + Ash.Type.storage_type(type, []) == :ci_string -> + Ecto.Query.dynamic(fragment("(? COLLATE NOCASE)", ^expr)) + + true -> + Ecto.Query.dynamic(type(^expr, ^Ash.Type.storage_type(type, []))) + end + end + + def type_expr(expr, type) do + case type do + {:parameterized, inner_type, constraints} -> + if inner_type.type(constraints) == :ci_string do + Ecto.Query.dynamic(fragment("(? COLLATE NOCASE)", ^expr)) + else + Ecto.Query.dynamic(type(^expr, ^type)) + end + + nil -> + expr + + type -> + Ecto.Query.dynamic(type(^expr, ^type)) + end + end + + @impl true + def table(resource) do + AshSqlite.DataLayer.Info.table(resource) + end + + @impl true + def schema(_resource) do + nil + end + + @impl true + def repo(resource, _kind) do + AshSqlite.DataLayer.Info.repo(resource) + end + + @impl true + def multicolumn_distinct?, do: false + + @impl true + def parameterized_type(type, constraints, no_maps? \\ false) + + def parameterized_type({:parameterized, _, _} = type, _, _) do + type + end + + def parameterized_type({:in, type}, constraints, no_maps?) do + parameterized_type({:array, type}, constraints, no_maps?) + end + + def parameterized_type({:array, type}, constraints, no_maps?) do + case parameterized_type(type, constraints[:items] || [], no_maps?) do + nil -> + nil + + type -> + {:array, type} + end + end + + def parameterized_type(type, _constraints, _no_maps?) + when type in [Ash.Type.Map, Ash.Type.Map.EctoType], + do: nil + + def parameterized_type(type, constraints, no_maps?) do + if Ash.Type.ash_type?(type) do + cast_in_query? = + if function_exported?(Ash.Type, :cast_in_query?, 2) do + Ash.Type.cast_in_query?(type, constraints) + else + Ash.Type.cast_in_query?(type) + end + + if cast_in_query? do + parameterized_type(Ash.Type.ecto_type(type), constraints, no_maps?) + else + nil + end + else + if is_atom(type) && :erlang.function_exported(type, :type, 1) do + {:parameterized, type, constraints || []} + else + type + end + end + end + + @impl true + def determine_types(mod, values) do + Code.ensure_compiled(mod) + + cond do + :erlang.function_exported(mod, :types, 0) -> + mod.types() + + :erlang.function_exported(mod, :args, 0) -> + mod.args() + + true -> + [:any] + end + |> Enum.map(fn types -> + case types do + :same -> + types = + for _ <- values do + :same + end + + closest_fitting_type(types, values) + + :any -> + for _ <- values do + :any + end + + types -> + closest_fitting_type(types, values) + end + end) + |> Enum.filter(fn types -> + Enum.all?(types, &(vagueness(&1) == 0)) + end) + |> case do + [type] -> + if type == :any || type == {:in, :any} do + nil + else + type + end + + # There are things we could likely do here + # We only say "we know what types these are" when we explicitly know + _ -> + Enum.map(values, fn _ -> nil end) + end + end + + defp closest_fitting_type(types, values) do + types_with_values = Enum.zip(types, values) + + types_with_values + |> fill_in_known_types() + |> clarify_types() + end + + defp clarify_types(types) do + basis = + types + |> Enum.map(&elem(&1, 0)) + |> Enum.min_by(&vagueness(&1)) + + Enum.map(types, fn {type, _value} -> + replace_same(type, basis) + end) + end + + defp replace_same({:in, type}, basis) do + {:in, replace_same(type, basis)} + end + + defp replace_same(:same, :same) do + :any + end + + defp replace_same(:same, {:in, :same}) do + {:in, :any} + end + + defp replace_same(:same, basis) do + basis + end + + defp replace_same(other, _basis) do + other + end + + defp fill_in_known_types(types) do + Enum.map(types, &fill_in_known_type/1) + end + + defp fill_in_known_type( + {vague_type, %Ash.Query.Ref{attribute: %{type: type, constraints: constraints}}} = ref + ) + when vague_type in [:any, :same] do + if Ash.Type.ash_type?(type) do + type = type |> parameterized_type(constraints, true) |> array_to_in() + + {type || :any, ref} + else + type = + if is_atom(type) && :erlang.function_exported(type, :type, 1) do + {:parameterized, type, []} |> array_to_in() + else + type |> array_to_in() + end + + {type, ref} + end + end + + defp fill_in_known_type( + {{:array, type}, %Ash.Query.Ref{attribute: %{type: {:array, type}} = attribute} = ref} + ) do + {:in, fill_in_known_type({type, %{ref | attribute: %{attribute | type: type}}})} + end + + defp fill_in_known_type({type, value}), do: {array_to_in(type), value} + + defp array_to_in({:array, v}), do: {:in, array_to_in(v)} + + defp array_to_in({:parameterized, type, constraints}), + do: {:parameterized, array_to_in(type), constraints} + + defp array_to_in(v), do: v + + defp vagueness({:in, type}), do: vagueness(type) + defp vagueness(:same), do: 2 + defp vagueness(:any), do: 1 + defp vagueness(_), do: 0 + + defp do_get_path( + query, + %Ash.Query.Function.GetPath{arguments: [left, right], embedded?: pred_embedded?}, + bindings, + embedded?, + acc, + type \\ nil + ) do + path = "$." <> Enum.join(right, ".") + + {expr, acc} = + AshSql.Expr.dynamic_expr( + query, + %Ash.Query.Function.Fragment{ + embedded?: pred_embedded?, + arguments: [ + raw: "json_extract(", + expr: left, + raw: ", ", + expr: path, + raw: ")" + ] + }, + bindings, + embedded?, + type, + acc + ) + + if type do + {expr, acc} = + AshSql.Expr.dynamic_expr( + query, + %Ash.Query.Function.Type{arguments: [expr, type, []]}, + bindings, + embedded?, + type, + acc + ) + + {:ok, expr, acc} + else + {:ok, expr, acc} + end + end + + defp determine_type_at_path(type, path) do + path + |> Enum.reject(&is_integer/1) + |> do_determine_type_at_path(type) + |> case do + nil -> + nil + + {type, constraints} -> + AshSqlite.Types.parameterized_type(type, constraints) + end + end + + defp do_determine_type_at_path([], _), do: nil + + defp do_determine_type_at_path([item], type) do + case Ash.Resource.Info.attribute(type, item) do + nil -> + nil + + %{type: {:array, type}, constraints: constraints} -> + constraints = constraints[:items] || [] + + {type, constraints} + + %{type: type, constraints: constraints} -> + {type, constraints} + end + end + + defp do_determine_type_at_path([item | rest], type) do + case Ash.Resource.Info.attribute(type, item) do + nil -> + nil + + %{type: {:array, type}} -> + if Ash.Type.embedded_type?(type) do + type + else + nil + end + + %{type: type} -> + if Ash.Type.embedded_type?(type) do + type + else + nil + end + end + |> case do + nil -> + nil + + type -> + do_determine_type_at_path(rest, type) + end + end +end diff --git a/lib/statement.ex b/lib/statement.ex new file mode 100644 index 0000000..506c963 --- /dev/null +++ b/lib/statement.ex @@ -0,0 +1,45 @@ +defmodule AshSqlite.Statement do + @moduledoc "Represents a custom statement to be run in generated migrations" + + @fields [ + :name, + :up, + :down, + :code? + ] + + defstruct @fields + + def fields, do: @fields + + @schema [ + name: [ + type: :atom, + required: true, + doc: """ + The name of the statement, must be unique within the resource + """ + ], + code?: [ + type: :boolean, + default: false, + doc: """ + By default, we place the strings inside of ecto migration's `execute/1` function and assume they are sql. Use this option if you want to provide custom elixir code to be placed directly in the migrations + """ + ], + up: [ + type: :string, + doc: """ + How to create the structure of the statement + """, + required: true + ], + down: [ + type: :string, + doc: "How to tear down the structure of the statement", + required: true + ] + ] + + def schema, do: @schema +end diff --git a/lib/transformers/ensure_table_or_polymorphic.ex b/lib/transformers/ensure_table_or_polymorphic.ex new file mode 100644 index 0000000..b6a4dd4 --- /dev/null +++ b/lib/transformers/ensure_table_or_polymorphic.ex @@ -0,0 +1,30 @@ +defmodule AshSqlite.Transformers.EnsureTableOrPolymorphic do + @moduledoc false + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + def transform(dsl) do + if Transformer.get_option(dsl, [:sqlite], :polymorphic?) || + Transformer.get_option(dsl, [:sqlite], :table) do + {:ok, dsl} + else + resource = Transformer.get_persisted(dsl, :module) + + raise Spark.Error.DslError, + module: resource, + message: """ + Must configure a table for #{inspect(resource)}. + + For example: + + ```elixir + sqlite do + table "the_table" + repo YourApp.Repo + end + ``` + """, + path: [:sqlite, :table] + end + end +end diff --git a/lib/transformers/validate_references.ex b/lib/transformers/validate_references.ex new file mode 100644 index 0000000..0d63a0b --- /dev/null +++ b/lib/transformers/validate_references.ex @@ -0,0 +1,23 @@ +defmodule AshSqlite.Transformers.ValidateReferences do + @moduledoc false + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + def after_compile?, do: true + + def transform(dsl) do + dsl + |> AshSqlite.DataLayer.Info.references() + |> Enum.each(fn reference -> + unless Ash.Resource.Info.relationship(dsl, reference.relationship) do + raise Spark.Error.DslError, + path: [:sqlite, :references, reference.relationship], + module: Transformer.get_persisted(dsl, :module), + message: + "Found reference configuration for relationship `#{reference.relationship}`, but no such relationship exists" + end + end) + + {:ok, dsl} + end +end diff --git a/lib/transformers/verify_repo.ex b/lib/transformers/verify_repo.ex new file mode 100644 index 0000000..5f49882 --- /dev/null +++ b/lib/transformers/verify_repo.ex @@ -0,0 +1,22 @@ +defmodule AshSqlite.Transformers.VerifyRepo do + @moduledoc false + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + def after_compile?, do: true + + def transform(dsl) do + repo = Transformer.get_option(dsl, [:sqlite], :repo) + + cond do + match?({:error, _}, Code.ensure_compiled(repo)) -> + {:error, "Could not find repo module #{repo}"} + + repo.__adapter__() != Ecto.Adapters.MyXQL -> + {:error, "Expected a repo using the MySQL adapter `Ecto.Adapters.MyXQL`"} + + true -> + {:ok, dsl} + end + end +end diff --git a/lib/type.ex b/lib/type.ex new file mode 100644 index 0000000..1be8904 --- /dev/null +++ b/lib/type.ex @@ -0,0 +1,19 @@ +defmodule AshSqlite.Type do + @moduledoc """ + Sqlite specific callbacks for `Ash.Type`. + + Use this in addition to `Ash.Type`. + """ + + @callback value_to_sqlite_default(Ash.Type.t(), Ash.Type.constraints(), term) :: + {:ok, String.t()} | :error + + defmacro __using__(_) do + quote do + @behaviour AshSqlite.Type + def value_to_sqlite_default(_, _, _), do: :error + + defoverridable value_to_sqlite_default: 3 + end + end +end diff --git a/lib/types/types.ex b/lib/types/types.ex new file mode 100644 index 0000000..63c5f3e --- /dev/null +++ b/lib/types/types.ex @@ -0,0 +1,182 @@ +defmodule AshSqlite.Types do + @moduledoc false + + alias Ash.Query.Ref + + def parameterized_type({:parameterized, _, _} = type, _) do + type + end + + def parameterized_type({:in, type}, constraints) do + parameterized_type({:array, type}, constraints) + end + + def parameterized_type({:array, type}, constraints) do + case parameterized_type(type, constraints[:items] || []) do + nil -> + nil + + type -> + {:array, type} + end + end + + def parameterized_type(type, _constraints) when type in [Ash.Type.Map, Ash.Type.Map.EctoType], + do: nil + + def parameterized_type(type, constraints) do + if Ash.Type.ash_type?(type) do + cast_in_query? = + if function_exported?(Ash.Type, :cast_in_query?, 2) do + Ash.Type.cast_in_query?(type, constraints) + else + Ash.Type.cast_in_query?(type) + end + + if cast_in_query? do + parameterized_type(Ash.Type.ecto_type(type), constraints) + else + nil + end + else + if is_atom(type) && :erlang.function_exported(type, :type, 1) do + {:parameterized, type, constraints || []} + else + type + end + end + end + + def determine_types(mod, values) do + Code.ensure_compiled(mod) + + cond do + :erlang.function_exported(mod, :types, 0) -> + mod.types() + + :erlang.function_exported(mod, :args, 0) -> + mod.args() + + true -> + [:any] + end + |> Enum.map(fn types -> + case types do + :same -> + types = + for _ <- values do + :same + end + + closest_fitting_type(types, values) + + :any -> + for _ <- values do + :any + end + + types -> + closest_fitting_type(types, values) + end + end) + |> Enum.filter(fn types -> + Enum.all?(types, &(vagueness(&1) == 0)) + end) + |> case do + [type] -> + if type == :any || type == {:in, :any} do + nil + else + type + end + + # There are things we could likely do here + # We only say "we know what types these are" when we explicitly know + _ -> + Enum.map(values, fn _ -> nil end) + end + end + + defp closest_fitting_type(types, values) do + types_with_values = Enum.zip(types, values) + + types_with_values + |> fill_in_known_types() + |> clarify_types() + end + + defp clarify_types(types) do + basis = + types + |> Enum.map(&elem(&1, 0)) + |> Enum.min_by(&vagueness(&1)) + + Enum.map(types, fn {type, _value} -> + replace_same(type, basis) + end) + end + + defp replace_same({:in, type}, basis) do + {:in, replace_same(type, basis)} + end + + defp replace_same(:same, :same) do + :any + end + + defp replace_same(:same, {:in, :same}) do + {:in, :any} + end + + defp replace_same(:same, basis) do + basis + end + + defp replace_same(other, _basis) do + other + end + + defp fill_in_known_types(types) do + Enum.map(types, &fill_in_known_type/1) + end + + defp fill_in_known_type( + {vague_type, %Ref{attribute: %{type: type, constraints: constraints}}} = ref + ) + when vague_type in [:any, :same] do + if Ash.Type.ash_type?(type) do + type = type |> parameterized_type(constraints) |> array_to_in() + + {type || :any, ref} + else + type = + if is_atom(type) && :erlang.function_exported(type, :type, 1) do + {:parameterized, type, []} |> array_to_in() + else + type |> array_to_in() + end + + {type, ref} + end + end + + defp fill_in_known_type( + {{:array, type}, %Ref{attribute: %{type: {:array, type}} = attribute} = ref} + ) do + {:in, fill_in_known_type({type, %{ref | attribute: %{attribute | type: type}}})} + end + + defp fill_in_known_type({type, value}), do: {array_to_in(type), value} + + defp array_to_in({:array, v}), do: {:in, array_to_in(v)} + + defp array_to_in({:parameterized, type, constraints}), + do: {:parameterized, array_to_in(type), constraints} + + defp array_to_in(v), do: v + + defp vagueness({:in, type}), do: vagueness(type) + defp vagueness(:same), do: 2 + defp vagueness(:any), do: 1 + defp vagueness(_), do: 0 +end diff --git a/logos/small-logo.png b/logos/small-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9fc9aa1801a2a5d20a6525f31fe7a619fb39f044 GIT binary patch literal 3088 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 D^yu%Z literal 0 HcmV?d00001 diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..fcad7b9 --- /dev/null +++ b/mix.exs @@ -0,0 +1,203 @@ +defmodule AshSqlite.MixProject do + use Mix.Project + + @description """ + The SQLite data layer for Ash Framework. + """ + + @version "0.1.2" + + def project do + [ + app: :ash_sqlite, + version: @version, + elixir: "~> 1.11", + start_permanent: Mix.env() == :prod, + deps: deps(), + description: @description, + elixirc_paths: elixirc_paths(Mix.env()), + preferred_cli_env: [ + coveralls: :test, + "coveralls.github": :test, + "test.create": :test, + "test.migrate": :test, + "test.rollback": :test, + "test.check_migrations": :test, + "test.drop": :test, + "test.generate_migrations": :test, + "test.reset": :test + ], + dialyzer: [ + plt_add_apps: [:ecto, :ash, :mix] + ], + docs: docs(), + aliases: aliases(), + package: package(), + source_url: "https://github.com/ash-project/ash_sqlite", + homepage_url: "https://github.com/ash-project/ash_sqlite", + consolidate_protocols: Mix.env() != :test + ] + end + + if Mix.env() == :test do + def application() do + [ + mod: {AshSqlite.TestApp, []} + ] + end + end + + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + defp package do + [ + name: :ash_sqlite, + licenses: ["MIT"], + files: ~w(lib .formatter.exs mix.exs README* LICENSE* + CHANGELOG* documentation), + links: %{ + GitHub: "https://github.com/ash-project/ash_sqlite" + } + ] + end + + defp docs do + [ + main: "readme", + source_ref: "v#{@version}", + logo: "logos/small-logo.png", + extras: [ + {"README.md", title: "Home"}, + "documentation/tutorials/getting-started-with-ash-sqlite.md", + "documentation/topics/about-ash-sqlite/what-is-ash-sqlite.md", + "documentation/topics/resources/references.md", + "documentation/topics/resources/polymorphic-resources.md", + "documentation/topics/development/migrations-and-tasks.md", + "documentation/topics/development/testing.md", + "documentation/topics/advanced/expressions.md", + "documentation/topics/advanced/manual-relationships.md", + "documentation/dsls/DSL:-AshSqlite.DataLayer.md", + "CHANGELOG.md" + ], + groups_for_extras: [ + Tutorials: [ + ~r'documentation/tutorials' + ], + "How To": ~r'documentation/how_to', + Topics: ~r'documentation/topics', + DSLs: ~r'documentation/dsls', + "About AshSqlite": [ + "CHANGELOG.md" + ] + ], + groups_for_modules: [ + AshSqlite: [ + AshSqlite, + AshSqlite.Repo, + AshSqlite.DataLayer + ], + Utilities: [ + AshSqlite.ManualRelationship + ], + Introspection: [ + AshSqlite.DataLayer.Info, + AshSqlite.CustomExtension, + AshSqlite.CustomIndex, + AshSqlite.Reference, + AshSqlite.Statement + ], + Types: [ + AshSqlite.Type + ], + Expressions: [ + AshSqlite.Functions.Fragment, + AshSqlite.Functions.Like + ], + Internals: ~r/.*/ + ] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:ecto_sql, "~> 3.9"}, + {:myxql, ">= 0.0.0"}, + {:ecto, "~> 3.9"}, + {:jason, "~> 1.0"}, + {:ash, ash_version("~> 3.0")}, + {:ash_sql, ash_sql_version("~> 0.1")}, + {:git_ops, "~> 2.5", only: [:dev, :test]}, + {:ex_doc, "~> 0.22", only: [:dev, :test], runtime: false}, + {:ex_check, "~> 0.14", only: [:dev, :test]}, + {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}, + {:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false}, + {:sobelow, ">= 0.0.0", only: [:dev, :test], runtime: false}, + {:mix_audit, ">= 0.0.0", only: [:dev, :test], runtime: false} + ] + end + + defp ash_version(default_version) do + case System.get_env("ASH_VERSION") do + nil -> + default_version + + "local" -> + [path: "../ash", override: true] + + "main" -> + [git: "https://github.com/ash-project/ash.git"] + + version when is_binary(version) -> + "~> #{version}" + + version -> + version + end + end + + defp ash_sql_version(default_version) do + case System.get_env("ASH_SQL_VERSION") do + nil -> + default_version + + "local" -> + [path: "../ash_sql", override: true] + + "main" -> + [git: "https://github.com/ash-project/ash_sql.git"] + + version when is_binary(version) -> + "~> #{version}" + + version -> + version + end + end + + defp aliases do + [ + sobelow: + "sobelow --skip -i Config.Secrets --ignore-files lib/migration_generator/migration_generator.ex", + credo: "credo --strict", + docs: [ + "spark.cheat_sheets", + "docs", + "spark.replace_doc_links", + "spark.cheat_sheets_in_search" + ], + "spark.formatter": "spark.formatter --extensions AshSqlite.DataLayer", + "spark.cheat_sheets": "spark.cheat_sheets --extensions AshSqlite.DataLayer", + "spark.cheat_sheets_in_search": + "spark.cheat_sheets_in_search --extensions AshSqlite.DataLayer", + "test.generate_migrations": "ash_sqlite.generate_migrations", + "test.check_migrations": "ash_sqlite.generate_migrations --check", + "test.migrate": "ash_sqlite.migrate", + "test.rollback": "ash_sqlite.rollback", + "test.create": "ash_sqlite.create", + "test.reset": ["test.drop", "test.create", "test.migrate"], + "test.drop": "ash_sqlite.drop" + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..5610185 --- /dev/null +++ b/mix.lock @@ -0,0 +1,42 @@ +%{ + "ash": {:hex, :ash, "3.0.0", "2ef88639fce9f126c57c115f955d7e29919942afe74adc00cb7250fd7ced9f5f", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {: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]}, {: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.8.1 and < 1.0.0-0", [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.1.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.6", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ffed4651c9faf79e90066afdd52202e3f8951624bf73fd8ad34aa4c22fceef4b"}, + "ash_sql": {:hex, :ash_sql, "0.1.1-rc.20", "54f1007c101ddce806a065e94d77890cb80a5c80fa485858334d33ccb753c620", [:mix], [{:ash, "~> 3.0.0-rc", [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", "26094d2fa92606e882399a16839815d2f9efe6cc06408c295c53525e3372c17c"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, + "comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"}, + "credo": {:hex, :credo, "1.7.6", "b8f14011a5443f2839b04def0b252300842ce7388f3af177157c86da18dfbeea", [: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", "146f347fb9f8cbc5f7e39e3f22f70acbef51d441baa6d10169dd604bfbc55296"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "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.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [: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", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, + "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 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", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.15.1", "40f2fbd9e246455f8c42e7e0a77009ef806caa1b3ce6f717b2a0a80e8432fcfd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.19", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "28b16e177123c688948357176662bf9ff9084daddf950ef5b6baf3ee93707064"}, + "elixir_make": {:hex, :elixir_make, "0.8.3", "d38d7ee1578d722d89b4d452a3e36bcfdc644c618f0d063b874661876e708683", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "5c99a18571a756d4af7a4d89ca75c28ac899e6103af6f223982f09ce44942cc9"}, + "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"}, + "ex_doc": {:hex, :ex_doc, "0.32.1", "21e40f939515373bcdc9cffe65f3b3543f05015ac6c3d01d991874129d173420", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5142c9db521f106d61ff33250f779807ed2a88620e472ac95dc7d59c380113da"}, + "exqlite": {:hex, :exqlite, "0.21.0", "8d06c60b3d6df42bb4cdeb4dce4bc804788e227cead7dc190c3ffaba50bffbb4", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "b177180bb2788b761ddd5949763640aef92ed06db80d70a1130b6bede180b45f"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, + "git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, + "mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"}, + "myxql": {:hex, :myxql, "0.6.4", "1502ea37ee23c31b79725b95d4cc3553693c2bda7421b1febc50722fd988c918", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a3307f4671f3009d3708283649adf205bfe280f7e036fc8ef7f16dbf821ab8e9"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "reactor": {:hex, :reactor, "0.8.2", "b2be82b1c3402537d06a8f85bb1849f72cb6b4be140495cb8956de7aec2fdebd", [:mix], [{: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", "c35eb23b77cc77ba922af108722ac93257899e35cfdd18882f0e659ad2cac9f3"}, + "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, + "sourceror": {:hex, :sourceror, "1.1.0", "9c129fa1bd7290014acf6f73e292f43938c17e3fccd7b7df6f41122cab45dda9", [:mix], [], "hexpm", "b9c348688e2cfc20acfef0feaca88643044be5acd2e0b02cf4a8d6ac1edc4c4a"}, + "spark": {:hex, :spark, "2.1.21", "0c2e5c24bc99f65ee874a563f9f3ba6e5c7c8a79b7de4b3b65af770ca6c8120e", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "e8a32fd3138524096553d908f37ddb23f0c7cb731d75018d5f2ad6cb583714ec"}, + "splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"}, + "stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"}, + "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, +} diff --git a/priv/resource_snapshots/test_repo/accounts/20240405234211.json b/priv/resource_snapshots/test_repo/accounts/20240405234211.json new file mode 100644 index 0000000..7b9ca04 --- /dev/null +++ b/priv/resource_snapshots/test_repo/accounts/20240405234211.json @@ -0,0 +1,62 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "boolean", + "source": "is_active", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "user_id", + "references": { + "name": "accounts_user_id_fkey", + "table": "users", + "on_delete": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": null, + "deferrable": false, + "destination_attribute_default": null, + "destination_attribute_generated": null + }, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "accounts", + "hash": "2320B8B55C597C2F07DED9B7BF714832FE22B0AA5E05959A4EA0553669BC368D", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/authors/20240405234211.json b/priv/resource_snapshots/test_repo/authors/20240405234211.json new file mode 100644 index 0000000..8e367c9 --- /dev/null +++ b/priv/resource_snapshots/test_repo/authors/20240405234211.json @@ -0,0 +1,70 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "first_name", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "last_name", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "map", + "source": "bio", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": [ + "array", + "text" + ], + "source": "badges", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "authors", + "hash": "EFBB1E574CC263E6E650121801C48B4370F1C9A7C8A213BEF111BFC769BF6651", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/comment_ratings/20240405234211.json b/priv/resource_snapshots/test_repo/comment_ratings/20240405234211.json new file mode 100644 index 0000000..2d4158d --- /dev/null +++ b/priv/resource_snapshots/test_repo/comment_ratings/20240405234211.json @@ -0,0 +1,62 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "bigint", + "source": "score", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "resource_id", + "references": { + "name": "comment_ratings_resource_id_fkey", + "table": "comments", + "on_delete": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": null, + "deferrable": false, + "destination_attribute_default": "nil", + "destination_attribute_generated": false + }, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "comment_ratings", + "hash": "88FFC6DC62CEA37397A9C16C51E43F6FF6EED6C34E4C529FFB4D20EF1BCFF98F", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/comments/20240405234211.json b/priv/resource_snapshots/test_repo/comments/20240405234211.json new file mode 100644 index 0000000..a8d033e --- /dev/null +++ b/priv/resource_snapshots/test_repo/comments/20240405234211.json @@ -0,0 +1,117 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "title", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "bigint", + "source": "likes", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "utc_datetime_usec", + "source": "arbitrary_timestamp", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "utc_datetime_usec", + "source": "created_at", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "post_id", + "references": { + "name": "special_name_fkey", + "table": "posts", + "on_delete": "delete", + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": "update", + "deferrable": false, + "destination_attribute_default": null, + "destination_attribute_generated": null + }, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "author_id", + "references": { + "name": "comments_author_id_fkey", + "table": "authors", + "on_delete": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": null, + "deferrable": false, + "destination_attribute_default": null, + "destination_attribute_generated": null + }, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "comments", + "hash": "4F081363C965C68A8E3CC755BCA058C9DC0FB18F5BE5B44FEBEB41B787727702", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/integer_posts/20240405234211.json b/priv/resource_snapshots/test_repo/integer_posts/20240405234211.json new file mode 100644 index 0000000..0b19d76 --- /dev/null +++ b/priv/resource_snapshots/test_repo/integer_posts/20240405234211.json @@ -0,0 +1,37 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "bigint", + "source": "id", + "references": null, + "allow_nil?": false, + "generated?": true, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "title", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "integer_posts", + "hash": "A3F61182D99B092A9D17E34B645823D8B0561B467B0195EFE0DA42947153D7E0", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/managers/20240405234211.json b/priv/resource_snapshots/test_repo/managers/20240405234211.json new file mode 100644 index 0000000..945168f --- /dev/null +++ b/priv/resource_snapshots/test_repo/managers/20240405234211.json @@ -0,0 +1,101 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "name", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "code", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "must_be_present", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "role", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "organization_id", + "references": { + "name": "managers_organization_id_fkey", + "table": "orgs", + "on_delete": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": null, + "deferrable": false, + "destination_attribute_default": null, + "destination_attribute_generated": null + }, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "managers", + "hash": "1A4EFC8497F6A73543858892D6324407A7060AC2585EDCA9A759D1E8AF509DEF", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [ + { + "name": "uniq_code", + "keys": [ + "code" + ], + "base_filter": null, + "index_name": "managers_uniq_code_index" + } + ], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/orgs/20240405234211.json b/priv/resource_snapshots/test_repo/orgs/20240405234211.json new file mode 100644 index 0000000..daee888 --- /dev/null +++ b/priv/resource_snapshots/test_repo/orgs/20240405234211.json @@ -0,0 +1,37 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "name", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "orgs", + "hash": "106CE7B860A710A1275B05F81F2272B74678DC467F87E4179F9BEA8BC979613C", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/post_links/20240405234211.json b/priv/resource_snapshots/test_repo/post_links/20240405234211.json new file mode 100644 index 0000000..bf22dc7 --- /dev/null +++ b/priv/resource_snapshots/test_repo/post_links/20240405234211.json @@ -0,0 +1,87 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "text", + "source": "state", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "source_post_id", + "references": { + "name": "post_links_source_post_id_fkey", + "table": "posts", + "on_delete": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": null, + "deferrable": false, + "destination_attribute_default": null, + "destination_attribute_generated": null + }, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "destination_post_id", + "references": { + "name": "post_links_destination_post_id_fkey", + "table": "posts", + "on_delete": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": null, + "deferrable": false, + "destination_attribute_default": null, + "destination_attribute_generated": null + }, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + } + ], + "table": "post_links", + "hash": "6ADC017A784C2619574DE223A15A29ECAF6D67C0543DF67A8E4E215E8F8ED300", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [ + { + "name": "unique_link", + "keys": [ + "source_post_id", + "destination_post_id" + ], + "base_filter": null, + "index_name": "post_links_unique_link_index" + } + ], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/post_ratings/20240405234211.json b/priv/resource_snapshots/test_repo/post_ratings/20240405234211.json new file mode 100644 index 0000000..8152559 --- /dev/null +++ b/priv/resource_snapshots/test_repo/post_ratings/20240405234211.json @@ -0,0 +1,62 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "bigint", + "source": "score", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "resource_id", + "references": { + "name": "post_ratings_resource_id_fkey", + "table": "posts", + "on_delete": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": null, + "deferrable": false, + "destination_attribute_default": "nil", + "destination_attribute_generated": false + }, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "post_ratings", + "hash": "73A4E0A79F5A6449FFE48E2469FDC275723EF207780DA9027F3BBE3119DC0FFA", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/post_views/20240405234211.json b/priv/resource_snapshots/test_repo/post_views/20240405234211.json new file mode 100644 index 0000000..c11833f --- /dev/null +++ b/priv/resource_snapshots/test_repo/post_views/20240405234211.json @@ -0,0 +1,47 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "utc_datetime_usec", + "source": "time", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "browser", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "post_id", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": false + } + ], + "table": "post_views", + "hash": "D0749D9F514E36781D95F2967C97860C58C6DEAE95543DFAAB0E9C09A1480E93", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/posts/20240405234211.json b/priv/resource_snapshots/test_repo/posts/20240405234211.json new file mode 100644 index 0000000..3eaa55d --- /dev/null +++ b/priv/resource_snapshots/test_repo/posts/20240405234211.json @@ -0,0 +1,261 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "title", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "bigint", + "source": "score", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "boolean", + "source": "public", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "citext", + "source": "category", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "type", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "bigint", + "source": "price", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "decimal", + "source": "decimal", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "status", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "status", + "source": "status_enum", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "map", + "source": "stuff", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "uniq_one", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "uniq_two", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "uniq_custom_one", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "uniq_custom_two", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "utc_datetime_usec", + "source": "created_at", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "utc_datetime_usec", + "source": "updated_at", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "organization_id", + "references": { + "name": "posts_organization_id_fkey", + "table": "orgs", + "on_delete": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": null, + "deferrable": false, + "destination_attribute_default": null, + "destination_attribute_generated": null + }, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "author_id", + "references": { + "name": "posts_author_id_fkey", + "table": "authors", + "on_delete": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": null, + "deferrable": false, + "destination_attribute_default": null, + "destination_attribute_generated": null + }, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "posts", + "hash": "00D35B64138747A522AD4EAB9BB8E09BDFE30C95844FD1D46E0951E85EA18FBE", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [ + { + "name": "uniq_one_and_two", + "keys": [ + "uniq_one", + "uniq_two" + ], + "base_filter": "type = 'sponsored'", + "index_name": "posts_uniq_one_and_two_index" + } + ], + "base_filter": "type = 'sponsored'", + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [ + { + "message": "dude what the heck", + "name": null, + "table": null, + "include": null, + "fields": [ + "uniq_custom_one", + "uniq_custom_two" + ], + "where": null, + "unique": true, + "using": null + } + ], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/profile/20240405234211.json b/priv/resource_snapshots/test_repo/profile/20240405234211.json new file mode 100644 index 0000000..5a32e97 --- /dev/null +++ b/priv/resource_snapshots/test_repo/profile/20240405234211.json @@ -0,0 +1,62 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "description", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "author_id", + "references": { + "name": "profile_author_id_fkey", + "table": "authors", + "on_delete": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": null, + "deferrable": false, + "destination_attribute_default": null, + "destination_attribute_generated": null + }, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "profile", + "hash": "710F812AC63D2051F6AB22912CE5304088AF1D8F03C2BAFDC07EB24FA62136C2", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/users/20240405234211.json b/priv/resource_snapshots/test_repo/users/20240405234211.json new file mode 100644 index 0000000..27c8593 --- /dev/null +++ b/priv/resource_snapshots/test_repo/users/20240405234211.json @@ -0,0 +1,62 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "generated?": false, + "primary_key?": true + }, + { + "default": "nil", + "size": null, + "type": "boolean", + "source": "is_active", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + }, + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "organization_id", + "references": { + "name": "users_organization_id_fkey", + "table": "orgs", + "on_delete": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "primary_key?": true, + "destination_attribute": "id", + "on_update": null, + "deferrable": false, + "destination_attribute_default": null, + "destination_attribute_generated": null + }, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "users", + "hash": "F1D2233C0B448A17B31E8971DEF529020894252BBF5BAFD58D7280FA36249071", + "repo": "Elixir.AshSqlite.TestRepo", + "identities": [], + "base_filter": null, + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/priv/test_repo/migrations/20240405234211_migrate_resources1.exs b/priv/test_repo/migrations/20240405234211_migrate_resources1.exs new file mode 100644 index 0000000..3c75ddd --- /dev/null +++ b/priv/test_repo/migrations/20240405234211_migrate_resources1.exs @@ -0,0 +1,231 @@ +defmodule AshSqlite.TestRepo.Migrations.MigrateResources1 do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_sqlite.generate_migrations` + """ + + use Ecto.Migration + + def up do + create table(:orgs, primary_key: false) do + add :name, :text + add :id, :uuid, null: false, primary_key: true + end + + create table(:authors, primary_key: false) do + #add :badges, {:array, :text} + add :bio, :map + add :last_name, :text + add :first_name, :text + add :id, :uuid, null: false, primary_key: true + end + + create table(:users, primary_key: false) do + add :organization_id, + references(:orgs, column: :id, name: "users_organization_id_fkey", type: :uuid) + + add :is_active, :boolean + add :id, :uuid, null: false, primary_key: true + end + + create table(:profile, primary_key: false) do + add :author_id, + references(:authors, column: :id, name: "profile_author_id_fkey", type: :uuid) + + add :description, :text + add :id, :uuid, null: false, primary_key: true + end + + create table(:posts, primary_key: false) do + add :author_id, references(:authors, column: :id, name: "posts_author_id_fkey", type: :uuid) + + add :organization_id, + references(:orgs, column: :id, name: "posts_organization_id_fkey", type: :uuid) + + add :updated_at, :utc_datetime_usec, null: false + add :created_at, :utc_datetime_usec, null: false + add :uniq_custom_two, :text + add :uniq_custom_one, :text + add :uniq_two, :text + add :uniq_one, :text + add :stuff, :map + add :status_enum, :"ENUM('open', 'closed')" + add :status, :text + add :decimal, :decimal + add :price, :bigint + add :type, :text + #add :category, :citext + add :category, :text + add :public, :boolean + add :score, :bigint + add :title, :text + add :id, :uuid, null: false, primary_key: true + end + + create table(:post_views, primary_key: false) do + add :post_id, :uuid, null: false + add :browser, :text + add :time, :utc_datetime_usec, null: false + end + + create table(:post_ratings, primary_key: false) do + add :resource_id, + references(:posts, column: :id, name: "post_ratings_resource_id_fkey", type: :uuid) + + add :score, :bigint + add :id, :uuid, null: false, primary_key: true + end + + create table(:post_links, primary_key: false) do + add :destination_post_id, + references(:posts, + column: :id, + name: "post_links_destination_post_id_fkey", + type: :uuid + ), + primary_key: true, + null: false + + add :source_post_id, + references(:posts, column: :id, name: "post_links_source_post_id_fkey", type: :uuid), + primary_key: true, + null: false + + add :state, :text + end + + create unique_index(:post_links, [:source_post_id, :destination_post_id], + name: "post_links_unique_link_index" + ) + + create table(:managers, primary_key: false) do + add :organization_id, + references(:orgs, column: :id, name: "managers_organization_id_fkey", type: :uuid) + + add :role, :text + add :must_be_present, :text, null: false + add :code, :text, null: false + add :name, :text + add :id, :uuid, null: false, primary_key: true + end + + create unique_index(:managers, ["code(768)"], name: "managers_uniq_code_index") + + create table(:integer_posts, primary_key: false) do + add :title, :text + add :id, :bigserial, null: false, primary_key: true + end + + create table(:comments, primary_key: false) do + add :author_id, + references(:authors, column: :id, name: "comments_author_id_fkey", type: :uuid) + + add :post_id, + references(:posts, + column: :id, + name: "special_name_fkey", + type: :uuid, + on_delete: :delete_all, + on_update: :update_all + ) + + add :created_at, :utc_datetime_usec, null: false + add :arbitrary_timestamp, :utc_datetime_usec + add :likes, :bigint + add :title, :text + add :id, :uuid, null: false, primary_key: true + end + + create table(:comment_ratings, primary_key: false) do + add :resource_id, + references(:comments, + column: :id, + name: "comment_ratings_resource_id_fkey", + type: :uuid + ) + + add :score, :bigint + add :id, :uuid, null: false, primary_key: true + end + + create index(:posts, ["uniq_custom_one(384)", "uniq_custom_two(384)"], unique: true) + + create unique_index(:posts, ["uniq_one(384)", "uniq_two(384)"], + # where: "type = 'sponsored'", + name: "posts_uniq_one_and_two_index" + ) + + create table(:accounts, primary_key: false) do + add :user_id, references(:users, column: :id, name: "accounts_user_id_fkey", type: :uuid) + add :is_active, :boolean + add :id, :uuid, null: false, primary_key: true + end + end + + def down do + drop constraint(:accounts, "accounts_user_id_fkey") + + drop table(:accounts) + + drop_if_exists unique_index(:posts, [:uniq_one, :uniq_two], + name: "posts_uniq_one_and_two_index" + ) + + drop_if_exists index(:posts, ["uniq_custom_one", "uniq_custom_two"], + name: "posts_uniq_custom_one_uniq_custom_two_index" + ) + + drop table(:authors) + + drop constraint(:comment_ratings, "comment_ratings_resource_id_fkey") + + drop table(:comment_ratings) + + drop constraint(:comments, "special_name_fkey") + + drop constraint(:comments, "comments_author_id_fkey") + + drop table(:comments) + + drop table(:integer_posts) + + drop_if_exists unique_index(:managers, [:code], name: "managers_uniq_code_index") + + drop constraint(:managers, "managers_organization_id_fkey") + + drop table(:managers) + + drop table(:orgs) + + drop_if_exists unique_index(:post_links, [:source_post_id, :destination_post_id], + name: "post_links_unique_link_index" + ) + + drop constraint(:post_links, "post_links_source_post_id_fkey") + + drop constraint(:post_links, "post_links_destination_post_id_fkey") + + drop table(:post_links) + + drop constraint(:post_ratings, "post_ratings_resource_id_fkey") + + drop table(:post_ratings) + + drop table(:post_views) + + drop constraint(:posts, "posts_organization_id_fkey") + + drop constraint(:posts, "posts_author_id_fkey") + + drop table(:posts) + + drop constraint(:profile, "profile_author_id_fkey") + + drop table(:profile) + + drop constraint(:users, "users_organization_id_fkey") + + drop table(:users) + end +end diff --git a/test/atomics_test.exs b/test/atomics_test.exs new file mode 100644 index 0000000..ae1ad57 --- /dev/null +++ b/test/atomics_test.exs @@ -0,0 +1,75 @@ +defmodule AshSqlite.AtomicsTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.Post + + import Ash.Expr + + test "atomics work on upserts" do + id = Ash.UUID.generate() + + Post + |> Ash.Changeset.for_create(:create, %{id: id, title: "foo", price: 1}, upsert?: true) + |> Ash.Changeset.atomic_update(:price, expr(price + 1)) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{id: id, title: "foo", price: 1}, upsert?: true) + |> Ash.Changeset.atomic_update(:price, expr(price + 1)) + |> Ash.create!() + + assert [%{price: 2}] = Post |> Ash.read!() + end + + test "a basic atomic works" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "foo", price: 1}) + |> Ash.create!() + + assert %{price: 2} = + post + |> Ash.Changeset.for_update(:update, %{}) + |> Ash.Changeset.atomic_update(:price, expr(price + 1)) + |> Ash.update!() + end + + test "an atomic that violates a constraint will return the proper error" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "foo", price: 1}) + |> Ash.create!() + + assert_raise Ash.Error.Invalid, ~r/does not exist/, fn -> + post + |> Ash.Changeset.for_update(:update, %{}) + |> Ash.Changeset.atomic_update(:organization_id, Ash.UUID.generate()) + |> Ash.update!() + end + end + + test "an atomic can refer to a calculation" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "foo", price: 1}) + |> Ash.create!() + + post = + post + |> Ash.Changeset.for_update(:update, %{}) + |> Ash.Changeset.atomic_update(:score, expr(score_after_winning)) + |> Ash.update!() + + assert post.score == 1 + end + + test "an atomic can be attached to an action" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "foo", price: 1}) + |> Ash.create!() + + assert Post.increment_score!(post, 2).score == 2 + + assert Post.increment_score!(post, 2).score == 4 + end +end diff --git a/test/bulk_create_test.exs b/test/bulk_create_test.exs new file mode 100644 index 0000000..4b6cda6 --- /dev/null +++ b/test/bulk_create_test.exs @@ -0,0 +1,143 @@ +defmodule AshSqlite.BulkCreateTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.Post + + describe "bulk creates" do + test "bulk creates insert each input" do + Ash.bulk_create!([%{title: "fred"}, %{title: "george"}], Post, :create) + + assert [%{title: "fred"}, %{title: "george"}] = + Post + |> Ash.Query.sort(:title) + |> Ash.read!() + end + + test "bulk creates can be streamed" do + assert [{:ok, %{title: "fred"}}, {:ok, %{title: "george"}}] = + Ash.bulk_create!([%{title: "fred"}, %{title: "george"}], Post, :create, + return_stream?: true, + return_records?: true + ) + |> Enum.sort_by(fn {:ok, result} -> result.title end) + end + + test "bulk creates can upsert" do + assert [ + {:ok, %{title: "fred", uniq_one: "one", uniq_two: "two", price: 10}}, + {:ok, %{title: "george", uniq_one: "three", uniq_two: "four", price: 20}} + ] = + Ash.bulk_create!( + [ + %{title: "fred", uniq_one: "one", uniq_two: "two", price: 10}, + %{title: "george", uniq_one: "three", uniq_two: "four", price: 20} + ], + Post, + :create, + return_stream?: true, + return_records?: true + ) + |> Enum.sort_by(fn {:ok, result} -> result.title end) + + assert [ + {:ok, %{title: "fred", uniq_one: "one", uniq_two: "two", price: 1000}}, + {:ok, %{title: "george", uniq_one: "three", uniq_two: "four", price: 20_000}} + ] = + Ash.bulk_create!( + [ + %{title: "something", uniq_one: "one", uniq_two: "two", price: 1000}, + %{title: "else", uniq_one: "three", uniq_two: "four", price: 20_000} + ], + Post, + :create, + upsert?: true, + upsert_identity: :uniq_one_and_two, + upsert_fields: [:price], + return_stream?: true, + return_records?: true + ) + |> Enum.sort_by(fn + {:ok, result} -> + result.title + + _ -> + nil + end) + end + + test "bulk creates can create relationships" do + Ash.bulk_create!( + [%{title: "fred", rating: %{score: 5}}, %{title: "george", rating: %{score: 0}}], + Post, + :create + ) + + assert [ + %{title: "fred", ratings: [%{score: 5}]}, + %{title: "george", ratings: [%{score: 0}]} + ] = + Post + |> Ash.Query.sort(:title) + |> Ash.Query.load(:ratings) + |> Ash.read!() + end + end + + describe "validation errors" do + test "skips invalid by default" do + assert %{records: [_], errors: [_]} = + Ash.bulk_create!([%{title: "fred"}, %{title: "not allowed"}], Post, :create, + return_records?: true, + return_errors?: true + ) + end + + test "returns errors in the stream" do + assert [{:ok, _}, {:error, _}] = + Ash.bulk_create!([%{title: "fred"}, %{title: "not allowed"}], Post, :create, + return_records?: true, + return_stream?: true, + return_errors?: true + ) + |> Enum.to_list() + end + end + + describe "database errors" do + test "database errors affect the entire batch" do + org = + AshSqlite.Test.Organization + |> Ash.Changeset.for_create(:create, %{name: "foo"}) + |> Ash.create!() + + Ash.bulk_create( + [ + %{title: "fred", organization_id: org.id}, + %{title: "george", organization_id: Ash.UUID.generate()} + ], + Post, + :create, + return_records?: true + ) + + assert [] = + Post + |> Ash.Query.sort(:title) + |> Ash.read!() + end + + test "database errors don't affect other batches" do + Ash.bulk_create( + [%{title: "george", organization_id: Ash.UUID.generate()}, %{title: "fred"}], + Post, + :create, + return_records?: true, + batch_size: 1 + ) + + assert [%{title: "fred"}] = + Post + |> Ash.Query.sort(:title) + |> Ash.read!() + end + end +end diff --git a/test/calculation_test.exs b/test/calculation_test.exs new file mode 100644 index 0000000..14a13ac --- /dev/null +++ b/test/calculation_test.exs @@ -0,0 +1,274 @@ +defmodule AshSqlite.CalculationTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.{Account, Author, Comment, Post, User} + + require Ash.Query + + test "calculations can refer to embedded attributes" do + author = + Author + |> Ash.Changeset.for_create(:create, %{bio: %{title: "Mr.", bio: "Bones"}}) + |> Ash.create!() + + assert %{title: "Mr."} = + Author + |> Ash.Query.filter(id == ^author.id) + |> Ash.Query.load(:title) + |> Ash.read_one!() + end + + test "calculations can use the || operator" do + author = + Author + |> Ash.Changeset.for_create(:create, %{bio: %{title: "Mr.", bio: "Bones"}}) + |> Ash.create!() + + assert %{first_name_or_bob: "bob"} = + Author + |> Ash.Query.filter(id == ^author.id) + |> Ash.Query.load(:first_name_or_bob) + |> Ash.read_one!() + end + + test "calculations can use the && operator" do + author = + Author + |> Ash.Changeset.for_create(:create, %{ + first_name: "fred", + bio: %{title: "Mr.", bio: "Bones"} + }) + |> Ash.create!() + + assert %{first_name_and_bob: "bob"} = + Author + |> Ash.Query.filter(id == ^author.id) + |> Ash.Query.load(:first_name_and_bob) + |> Ash.read_one!() + end + + test "concat calculation can be filtered on" do + author = + Author + |> Ash.Changeset.for_create(:create, %{first_name: "is", last_name: "match"}) + |> Ash.create!() + + Author + |> Ash.Changeset.for_create(:create, %{first_name: "not", last_name: "match"}) + |> Ash.create!() + + author_id = author.id + + assert %{id: ^author_id} = + Author + |> Ash.Query.load(:full_name) + |> Ash.Query.filter(full_name == "is match") + |> Ash.read_one!() + end + + test "conditional calculations can be filtered on" do + author = + Author + |> Ash.Changeset.for_create(:create, %{first_name: "tom"}) + |> Ash.create!() + + Author + |> Ash.Changeset.for_create(:create, %{first_name: "tom", last_name: "holland"}) + |> Ash.create!() + + author_id = author.id + + assert %{id: ^author_id} = + Author + |> Ash.Query.load([:conditional_full_name, :full_name]) + |> Ash.Query.filter(conditional_full_name == "(none)") + |> Ash.read_one!() + end + + test "parameterized calculations can be filtered on" do + Author + |> Ash.Changeset.for_create(:create, %{first_name: "tom", last_name: "holland"}) + |> Ash.create!() + + assert %{param_full_name: "tom holland"} = + Author + |> Ash.Query.load(:param_full_name) + |> Ash.read_one!() + + assert %{param_full_name: "tom~holland"} = + Author + |> Ash.Query.load(param_full_name: [separator: "~"]) + |> Ash.read_one!() + + assert %{} = + Author + |> Ash.Query.filter(param_full_name(separator: "~") == "tom~holland") + |> Ash.read_one!() + end + + test "parameterized related calculations can be filtered on" do + author = + Author + |> Ash.Changeset.for_create(:create, %{first_name: "tom", last_name: "holland"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "match"}) + |> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove) + |> Ash.create!() + + assert %{title: "match"} = + Comment + |> Ash.Query.filter(author.param_full_name(separator: "~") == "tom~holland") + |> Ash.read_one!() + + assert %{title: "match"} = + Comment + |> Ash.Query.filter( + author.param_full_name(separator: "~") == "tom~holland" and + author.param_full_name(separator: " ") == "tom holland" + ) + |> Ash.read_one!() + end + + test "parameterized calculations can be sorted on" do + Author + |> Ash.Changeset.for_create(:create, %{first_name: "tom", last_name: "holland"}) + |> Ash.create!() + + Author + |> Ash.Changeset.for_create(:create, %{first_name: "abc", last_name: "def"}) + |> Ash.create!() + + assert [%{first_name: "abc"}, %{first_name: "tom"}] = + Author + |> Ash.Query.sort(param_full_name: [separator: "~"]) + |> Ash.read!() + end + + test "calculations using if and literal boolean results can run" do + Post + |> Ash.Query.load(:was_created_in_the_last_month) + |> Ash.Query.filter(was_created_in_the_last_month == true) + |> Ash.read!() + end + + test "nested conditional calculations can be loaded" do + Author + |> Ash.Changeset.for_create(:create, %{last_name: "holland"}) + |> Ash.create!() + + Author + |> Ash.Changeset.for_create(:create, %{first_name: "tom"}) + |> Ash.create!() + + assert [%{nested_conditional: "No First Name"}, %{nested_conditional: "No Last Name"}] = + Author + |> Ash.Query.load(:nested_conditional) + |> Ash.Query.sort(:nested_conditional) + |> Ash.read!() + end + + test "loading a calculation loads its dependent loads" do + user = + User + |> Ash.Changeset.for_create(:create, %{is_active: true}) + |> Ash.create!() + + account = + Account + |> Ash.Changeset.for_create(:create, %{is_active: true}) + |> Ash.Changeset.manage_relationship(:user, user, type: :append_and_remove) + |> Ash.create!() + |> Ash.load!([:active]) + + assert account.active + end + + describe "-/1" do + test "makes numbers negative" do + Post + |> Ash.Changeset.for_create(:create, %{title: "match", score: 42}) + |> Ash.create!() + + assert [%{negative_score: -42}] = + Post + |> Ash.Query.load(:negative_score) + |> Ash.read!() + end + end + + describe "maps" do + test "maps can be constructed" do + Post + |> Ash.Changeset.for_create(:create, %{title: "match", score: 42}) + |> Ash.create!() + + assert [%{score_map: %{negative_score: %{foo: -42}}}] = + Post + |> Ash.Query.load(:score_map) + |> Ash.read!() + end + end + + test "dependent calc" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "match", price: 10_024}) + |> Ash.create!() + + Post.get_by_id(post.id, + query: Post |> Ash.Query.select([:id]) |> Ash.Query.load([:price_string_with_currency_sign]) + ) + end + + test "nested get_path works" do + assert "thing" = + Post + |> Ash.Changeset.for_create(:create, %{ + title: "match", + price: 10_024, + stuff: %{foo: %{bar: "thing"}} + }) + |> Ash.Changeset.deselect(:stuff) + |> Ash.create!() + |> Ash.load!(:foo_bar_from_stuff) + |> Map.get(:foo_bar_from_stuff) + end + + test "contains uses instr" do + Post + |> Ash.Changeset.for_create(:create, %{ + title: "foo-dude-bar" + }) + |> Ash.create!() + + assert Post + |> Ash.Query.filter(contains(title, "-dude-")) + |> Ash.read_one!() + end + + test "runtime expression calcs" do + author = + Author + |> Ash.Changeset.for_create(:create, %{ + first_name: "Bill", + last_name: "Jones", + bio: %{title: "Mr.", bio: "Bones"} + }) + |> Ash.create!() + + assert %AshSqlite.Test.Money{} = + Post + |> Ash.Changeset.for_create(:create, %{title: "match", price: 10_024}) + |> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove) + |> Ash.create!() + |> Ash.load!(:calc_returning_json) + |> Map.get(:calc_returning_json) + + assert [%AshSqlite.Test.Money{}] = + author + |> Ash.load!(posts: :calc_returning_json) + |> Map.get(:posts) + |> Enum.map(&Map.get(&1, :calc_returning_json)) + end +end diff --git a/test/custom_index_test.exs b/test/custom_index_test.exs new file mode 100644 index 0000000..d45b0f4 --- /dev/null +++ b/test/custom_index_test.exs @@ -0,0 +1,28 @@ +defmodule AshSqlite.Test.CustomIndexTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.Post + + require Ash.Query + + test "unique constraint errors are properly caught" do + Post + |> Ash.Changeset.for_create(:create, %{ + title: "first", + uniq_custom_one: "what", + uniq_custom_two: "what2" + }) + |> Ash.create!() + + assert_raise Ash.Error.Invalid, + ~r/Invalid value provided for uniq_custom_one: dude what the heck/, + fn -> + Post + |> Ash.Changeset.for_create(:create, %{ + title: "first", + uniq_custom_one: "what", + uniq_custom_two: "what2" + }) + |> Ash.create!() + end + end +end diff --git a/test/ecto_compatibility_test.exs b/test/ecto_compatibility_test.exs new file mode 100644 index 0000000..e47f805 --- /dev/null +++ b/test/ecto_compatibility_test.exs @@ -0,0 +1,15 @@ +defmodule AshSqlite.EctoCompatibilityTest do + use AshSqlite.RepoCase, async: false + require Ash.Query + + test "call Ecto.Repo.insert! via Ash Repo" do + org = + %AshSqlite.Test.Organization{ + id: Ash.UUID.generate(), + name: "The Org" + } + |> AshSqlite.TestRepo.insert!() + + assert org.name == "The Org" + end +end diff --git a/test/embeddable_resource_test.exs b/test/embeddable_resource_test.exs new file mode 100644 index 0000000..8bb95c3 --- /dev/null +++ b/test/embeddable_resource_test.exs @@ -0,0 +1,34 @@ +defmodule AshSqlite.EmbeddableResourceTest do + @moduledoc false + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.{Author, Bio, Post} + + require Ash.Query + + setup do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + %{post: post} + end + + test "calculations can load json", %{post: post} do + assert %{calc_returning_json: %AshSqlite.Test.Money{amount: 100, currency: :usd}} = + Ash.load!(post, :calc_returning_json) + end + + test "embeds with list attributes set to nil are loaded as nil" do + post = + Author + |> Ash.Changeset.for_create(:create, %{bio: %Bio{list_of_strings: nil}}) + |> Ash.create!() + + assert is_nil(post.bio.list_of_strings) + + post = Ash.reload!(post) + + assert is_nil(post.bio.list_of_strings) + end +end diff --git a/test/enum_test.exs b/test/enum_test.exs new file mode 100644 index 0000000..a0cff4b --- /dev/null +++ b/test/enum_test.exs @@ -0,0 +1,13 @@ +defmodule AshSqlite.EnumTest do + @moduledoc false + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.Post + + require Ash.Query + + test "valid values are properly inserted" do + Post + |> Ash.Changeset.for_create(:create, %{title: "title", status: :open}) + |> Ash.create!() + end +end diff --git a/test/filter_test.exs b/test/filter_test.exs new file mode 100644 index 0000000..6fc8095 --- /dev/null +++ b/test/filter_test.exs @@ -0,0 +1,655 @@ +defmodule AshSqlite.FilterTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.{Author, Comment, Post} + + require Ash.Query + + describe "with no filter applied" do + test "with no data" do + assert [] = Ash.read!(Post) + end + + test "with data" do + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + assert [%Post{title: "title"}] = Ash.read!(Post) + end + end + + describe "invalid uuid" do + test "with an invalid uuid, an invalid error is raised" do + assert_raise Ash.Error.Invalid, fn -> + Post + |> Ash.Query.filter(id == "foo") + |> Ash.read!() + end + end + end + + describe "with a simple filter applied" do + test "with no data" do + results = + Post + |> Ash.Query.filter(title == "title") + |> Ash.read!() + + assert [] = results + end + + test "with data that matches" do + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(title == "title") + |> Ash.read!() + + assert [%Post{title: "title"}] = results + end + + test "with some data that matches and some data that doesnt" do + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(title == "no_title") + |> Ash.read!() + + assert [] = results + end + + test "with related data that doesn't match" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "not match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(comments.title == "match") + |> Ash.read!() + + assert [] = results + end + + test "with related data two steps away that matches" do + author = + Author + |> Ash.Changeset.for_create(:create, %{first_name: "match"}) + |> Ash.create!() + + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "title2"}) + |> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove) + |> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "not match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove) + |> Ash.create!() + + results = + Comment + |> Ash.Query.filter(author.posts.linked_posts.title == "title") + |> Ash.read!() + + assert [_] = results + end + + test "with related data that does match" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(comments.title == "match") + |> Ash.read!() + + assert [%Post{title: "title"}] = results + end + + test "with related data that does and doesn't match" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "not match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(comments.title == "match") + |> Ash.read!() + + assert [%Post{title: "title"}] = results + end + end + + describe "in" do + test "it properly filters" do + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "title1"}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "title2"}) + |> Ash.create!() + + assert [%Post{title: "title1"}, %Post{title: "title2"}] = + Post + |> Ash.Query.filter(title in ["title1", "title2"]) + |> Ash.Query.sort(title: :asc) + |> Ash.read!() + end + end + + describe "with a boolean filter applied" do + test "with no data" do + results = + Post + |> Ash.Query.filter(title == "title" or score == 1) + |> Ash.read!() + + assert [] = results + end + + test "with data that doesn't match" do + Post + |> Ash.Changeset.for_create(:create, %{title: "no title", score: 2}) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(title == "title" or score == 1) + |> Ash.read!() + + assert [] = results + end + + test "with data that matches both conditions" do + Post + |> Ash.Changeset.for_create(:create, %{title: "title", score: 0}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{score: 1, title: "nothing"}) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(title == "title" or score == 1) + |> Ash.read!() + |> Enum.sort_by(& &1.score) + + assert [%Post{title: "title", score: 0}, %Post{title: "nothing", score: 1}] = results + end + + test "with data that matches one condition and data that matches nothing" do + Post + |> Ash.Changeset.for_create(:create, %{title: "title", score: 0}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{score: 2, title: "nothing"}) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(title == "title" or score == 1) + |> Ash.read!() + |> Enum.sort_by(& &1.score) + + assert [%Post{title: "title", score: 0}] = results + end + + test "with related data in an or statement that matches, while basic filter doesn't match" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "doesn't match"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(title == "match" or comments.title == "match") + |> Ash.read!() + + assert [%Post{title: "doesn't match"}] = results + end + + test "with related data in an or statement that doesn't match, while basic filter does match" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "match"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "doesn't match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(title == "match" or comments.title == "match") + |> Ash.read!() + + assert [%Post{title: "match"}] = results + end + + test "with related data and an inner join condition" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "match"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(title == comments.title) + |> Ash.read!() + + assert [%Post{title: "match"}] = results + + results = + Post + |> Ash.Query.filter(title != comments.title) + |> Ash.read!() + + assert [] = results + end + end + + describe "accessing embeds" do + setup do + Author + |> Ash.Changeset.for_create(:create, + bio: %{title: "Dr.", bio: "Strange", years_of_experience: 10} + ) + |> Ash.create!() + + Author + |> Ash.Changeset.for_create(:create, + bio: %{title: "Highlander", bio: "There can be only one."} + ) + |> Ash.create!() + + :ok + end + + test "works using simple equality" do + assert [%{bio: %{title: "Dr."}}] = + Author + |> Ash.Query.filter(bio[:title] == "Dr.") + |> Ash.read!() + end + + test "works using simple equality for integers" do + assert [%{bio: %{title: "Dr."}}] = + Author + |> Ash.Query.filter(bio[:years_of_experience] == 10) + |> Ash.read!() + end + + test "calculations that use embeds can be filtered on" do + assert [%{bio: %{title: "Dr."}}] = + Author + |> Ash.Query.filter(title == "Dr.") + |> Ash.read!() + end + end + + describe "basic expressions" do + test "basic expressions work" do + Post + |> Ash.Changeset.for_create(:create, %{title: "match", score: 4}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "non_match", score: 2}) + |> Ash.create!() + + assert [%{title: "match"}] = + Post + |> Ash.Query.filter(score + 1 == 5) + |> Ash.read!() + end + end + + describe "case insensitive fields" do + test "it matches case insensitively" do + Post + |> Ash.Changeset.for_create(:create, %{title: "match", category: "FoObAr"}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{category: "bazbuz"}) + |> Ash.create!() + + assert [%{title: "match"}] = + Post + |> Ash.Query.filter(category == "fOoBaR") + |> Ash.read!() + end + end + + describe "exists/2" do + test "it works with single relationships" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "match"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "abba"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + post2 = + Post + |> Ash.Changeset.for_create(:create, %{title: "no_match"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "acca"}) + |> Ash.Changeset.manage_relationship(:post, post2, type: :append_and_remove) + |> Ash.create!() + + assert [%{title: "match"}] = + Post + |> Ash.Query.filter(exists(comments, title == ^"abba")) + |> Ash.read!() + end + + test "it works with many to many relationships" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "a"}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "b"}) + |> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove) + |> Ash.create!() + + assert [%{title: "b"}] = + Post + |> Ash.Query.filter(exists(linked_posts, title == ^"a")) + |> Ash.read!() + end + + test "it works with join association relationships" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "a"}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "b"}) + |> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove) + |> Ash.create!() + + assert [%{title: "b"}] = + Post + |> Ash.Query.filter(exists(linked_posts, title == ^"a")) + |> Ash.read!() + end + + test "it works with nested relationships as the path" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "a"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "comment"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "b"}) + |> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove) + |> Ash.create!() + + assert [%{title: "b"}] = + Post + |> Ash.Query.filter(exists(linked_posts.comments, title == ^"comment")) + |> Ash.read!() + end + + test "it works with an `at_path`" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "a"}) + |> Ash.create!() + + other_post = + Post + |> Ash.Changeset.for_create(:create, %{title: "other_a"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "comment"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "comment"}) + |> Ash.Changeset.manage_relationship(:post, other_post, type: :append_and_remove) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "b"}) + |> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "b"}) + |> Ash.Changeset.manage_relationship(:linked_posts, [other_post], type: :append_and_remove) + |> Ash.create!() + + assert [%{title: "b"}] = + Post + |> Ash.Query.filter( + linked_posts.title == "a" and + linked_posts.exists(comments, title == ^"comment") + ) + |> Ash.read!() + + assert [%{title: "b"}] = + Post + |> Ash.Query.filter( + linked_posts.title == "a" and + linked_posts.exists(comments, title == ^"comment") + ) + |> Ash.read!() + end + + test "it works with nested relationships inside of exists" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "a"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "comment"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "b"}) + |> Ash.Changeset.manage_relationship(:linked_posts, [post], type: :append_and_remove) + |> Ash.create!() + + assert [%{title: "b"}] = + Post + |> Ash.Query.filter(exists(linked_posts, comments.title == ^"comment")) + |> Ash.read!() + end + end + + describe "filtering on enum types" do + test "it allows simple filtering" do + Post + |> Ash.Changeset.for_create(:create, status_enum: "open") + |> Ash.create!() + + assert %{status_enum: :open} = + Post + |> Ash.Query.filter(status_enum == ^"open") + |> Ash.read_one!() + end + + test "it allows simple filtering without casting" do + Post + |> Ash.Changeset.for_create(:create, status_enum_no_cast: "open") + |> Ash.create!() + + assert %{status_enum_no_cast: :open} = + Post + |> Ash.Query.filter(status_enum_no_cast == ^"open") + |> Ash.read_one!() + end + end + + describe "atom filters" do + test "it works on matches" do + Post + |> Ash.Changeset.for_create(:create, %{title: "match"}) + |> Ash.create!() + + result = + Post + |> Ash.Query.filter(type == :sponsored) + |> Ash.read!() + + assert [%Post{title: "match"}] = result + end + end + + describe "like" do + test "like builds and matches" do + Post + |> Ash.Changeset.for_create(:create, %{title: "MaTcH"}) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(like(title, "%aTc%")) + |> Ash.read!() + + assert [%Post{title: "MaTcH"}] = results + + results = + Post + |> Ash.Query.filter(like(title, "%atc%")) + |> Ash.read!() + + assert [] = results + end + end + + describe "ilike" do + test "ilike builds and matches" do + Post + |> Ash.Changeset.for_create(:create, %{title: "MaTcH"}) + |> Ash.create!() + + results = + Post + |> Ash.Query.filter(ilike(title, "%aTc%")) + |> Ash.read!() + + assert [%Post{title: "MaTcH"}] = results + + results = + Post + |> Ash.Query.filter(ilike(title, "%atc%")) + |> Ash.read!() + + assert [%Post{title: "MaTcH"}] = results + end + end + + describe "fragments" do + test "double replacement works" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "match", score: 4}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "non_match", score: 2}) + |> Ash.create!() + + assert [%{title: "match"}] = + Post + |> Ash.Query.filter(fragment("? = ?", title, ^post.title)) + |> Ash.read!() + + assert [] = + Post + |> Ash.Query.filter(fragment("? = ?", title, "nope")) + |> Ash.read!() + end + end + + describe "filtering on relationships that themselves have filters" do + test "it doesn't raise an error" do + Comment + |> Ash.Query.filter(not is_nil(popular_ratings.id)) + |> Ash.read!() + end + + test "it doesn't raise an error when nested" do + Post + |> Ash.Query.filter(not is_nil(comments.popular_ratings.id)) + |> Ash.read!() + end + end +end diff --git a/test/load_test.exs b/test/load_test.exs new file mode 100644 index 0000000..495636d --- /dev/null +++ b/test/load_test.exs @@ -0,0 +1,247 @@ +defmodule AshSqlite.Test.LoadTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.{Comment, Post} + + require Ash.Query + + test "has_many relationships can be loaded" do + assert %Post{comments: %Ash.NotLoaded{type: :relationship}} = + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + results = + Post + |> Ash.Query.load(:comments) + |> Ash.read!() + + assert [%Post{comments: [%{title: "match"}]}] = results + end + + test "belongs_to relationships can be loaded" do + assert %Comment{post: %Ash.NotLoaded{type: :relationship}} = + comment = + Comment + |> Ash.Changeset.for_create(:create, %{}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "match"}) + |> Ash.Changeset.manage_relationship(:comments, [comment], type: :append_and_remove) + |> Ash.create!() + + results = + Comment + |> Ash.Query.load(:post) + |> Ash.read!() + + assert [%Comment{post: %{title: "match"}}] = results + end + + test "many_to_many loads work" do + source_post = + Post + |> Ash.Changeset.for_create(:create, %{title: "source"}) + |> Ash.create!() + + destination_post = + Post + |> Ash.Changeset.for_create(:create, %{title: "destination"}) + |> Ash.create!() + + destination_post2 = + Post + |> Ash.Changeset.for_create(:create, %{title: "destination"}) + |> Ash.create!() + + source_post + |> Ash.Changeset.new() + |> Ash.Changeset.manage_relationship(:linked_posts, [destination_post, destination_post2], + type: :append_and_remove + ) + |> Ash.update!() + + results = + source_post + |> Ash.load!(:linked_posts) + + assert %{linked_posts: [%{title: "destination"}, %{title: "destination"}]} = results + end + + test "many_to_many loads work when nested" do + source_post = + Post + |> Ash.Changeset.for_create(:create, %{title: "source"}) + |> Ash.create!() + + destination_post = + Post + |> Ash.Changeset.for_create(:create, %{title: "destination"}) + |> Ash.create!() + + source_post + |> Ash.Changeset.new() + |> Ash.Changeset.manage_relationship(:linked_posts, [destination_post], + type: :append_and_remove + ) + |> Ash.update!() + + destination_post + |> Ash.Changeset.new() + |> Ash.Changeset.manage_relationship(:linked_posts, [source_post], type: :append_and_remove) + |> Ash.update!() + + results = + source_post + |> Ash.load!(linked_posts: :linked_posts) + + assert %{linked_posts: [%{title: "destination", linked_posts: [%{title: "source"}]}]} = + results + end + + describe "lateral join loads" do + # uncomment when lateral join is supported + # it does not necessarily have to be implemented *exactly* as lateral join + # test "parent references are resolved" do + # post1 = + # Post + # |> Ash.Changeset.new(%{title: "title"}) + # |> Api.create!() + + # post2 = + # Post + # |> Ash.Changeset.new(%{title: "title"}) + # |> Api.create!() + + # post2_id = post2.id + + # post3 = + # Post + # |> Ash.Changeset.new(%{title: "no match"}) + # |> Api.create!() + + # assert [%{posts_with_matching_title: [%{id: ^post2_id}]}] = + # Post + # |> Ash.Query.load(:posts_with_matching_title) + # |> Ash.Query.filter(id == ^post1.id) + # |> Api.read!() + + # assert [%{posts_with_matching_title: []}] = + # Post + # |> Ash.Query.load(:posts_with_matching_title) + # |> Ash.Query.filter(id == ^post3.id) + # |> Api.read!() + # end + + # test "parent references work when joining for filters" do + # %{id: post1_id} = + # Post + # |> Ash.Changeset.new(%{title: "title"}) + # |> Api.create!() + + # post2 = + # Post + # |> Ash.Changeset.new(%{title: "title"}) + # |> Api.create!() + + # Post + # |> Ash.Changeset.new(%{title: "no match"}) + # |> Api.create!() + + # Post + # |> Ash.Changeset.new(%{title: "no match"}) + # |> Api.create!() + + # assert [%{id: ^post1_id}] = + # Post + # |> Ash.Query.filter(posts_with_matching_title.id == ^post2.id) + # |> Api.read!() + # end + + # test "lateral join loads (loads with limits or offsets) are supported" do + # assert %Post{comments: %Ash.NotLoaded{type: :relationship}} = + # post = + # Post + # |> Ash.Changeset.new(%{title: "title"}) + # |> Api.create!() + + # Comment + # |> Ash.Changeset.new(%{title: "abc"}) + # |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + # |> Api.create!() + + # Comment + # |> Ash.Changeset.new(%{title: "def"}) + # |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + # |> Api.create!() + + # comments_query = + # Comment + # |> Ash.Query.limit(1) + # |> Ash.Query.sort(:title) + + # results = + # Post + # |> Ash.Query.load(comments: comments_query) + # |> Api.read!() + + # assert [%Post{comments: [%{title: "abc"}]}] = results + + # comments_query = + # Comment + # |> Ash.Query.limit(1) + # |> Ash.Query.sort(title: :desc) + + # results = + # Post + # |> Ash.Query.load(comments: comments_query) + # |> Api.read!() + + # assert [%Post{comments: [%{title: "def"}]}] = results + + # comments_query = + # Comment + # |> Ash.Query.limit(2) + # |> Ash.Query.sort(title: :desc) + + # results = + # Post + # |> Ash.Query.load(comments: comments_query) + # |> Api.read!() + + # assert [%Post{comments: [%{title: "def"}, %{title: "abc"}]}] = results + # end + + test "loading many to many relationships on records works without loading its join relationship when using code interface" do + source_post = + Post + |> Ash.Changeset.for_create(:create, %{title: "source"}) + |> Ash.create!() + + destination_post = + Post + |> Ash.Changeset.for_create(:create, %{title: "abc"}) + |> Ash.create!() + + destination_post2 = + Post + |> Ash.Changeset.for_create(:create, %{title: "def"}) + |> Ash.create!() + + source_post + |> Ash.Changeset.new() + |> Ash.Changeset.manage_relationship(:linked_posts, [destination_post, destination_post2], + type: :append_and_remove + ) + |> Ash.update!() + + assert %{linked_posts: [_, _]} = Post.get_by_id!(source_post.id, load: [:linked_posts]) + end + end +end diff --git a/test/manual_relationships_test.exs b/test/manual_relationships_test.exs new file mode 100644 index 0000000..5eaafe5 --- /dev/null +++ b/test/manual_relationships_test.exs @@ -0,0 +1,116 @@ +defmodule AshSqlite.Test.ManualRelationshipsTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.{Comment, Post} + + require Ash.Query + + describe "manual first" do + test "relationships can be filtered on with no data" do + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + assert [] = + Post |> Ash.Query.filter(comments_containing_title.title == "title") |> Ash.read!() + end + + test "relationships can be filtered on with data" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "title2"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "title2"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "no match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + assert [_] = + Post + |> Ash.Query.filter(comments_containing_title.title == "title2") + |> Ash.read!() + end + end + + describe "manual last" do + test "relationships can be filtered on with no data" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "no match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + assert [] = + Comment + |> Ash.Query.filter(post.comments_containing_title.title == "title2") + |> Ash.read!() + end + + test "relationships can be filtered on with data" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "title2"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "title2"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "no match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + assert [_, _] = + Comment + |> Ash.Query.filter(post.comments_containing_title.title == "title2") + |> Ash.read!() + end + end + + describe "manual middle" do + test "relationships can be filtered on with data" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "title2"}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "title2"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "no match"}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + assert [_, _] = + Comment + |> Ash.Query.filter(post.comments_containing_title.post.title == "title") + |> Ash.read!() + end + end +end diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs new file mode 100644 index 0000000..ee21de0 --- /dev/null +++ b/test/migration_generator_test.exs @@ -0,0 +1,816 @@ +defmodule AshSqlite.MigrationGeneratorTest do + use AshSqlite.RepoCase, async: false + @moduletag :migration + + import ExUnit.CaptureLog + + defmacrop defposts(mod \\ Post, do: body) do + quote do + Code.compiler_options(ignore_module_conflict: true) + + defmodule unquote(mod) do + use Ash.Resource, + domain: nil, + data_layer: AshSqlite.DataLayer + + sqlite do + table "posts" + repo(AshSqlite.TestRepo) + + custom_indexes do + # need one without any opts + index(["id"]) + index(["id"], unique: true, name: "test_unique_index") + end + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + + unquote(body) + end + + Code.compiler_options(ignore_module_conflict: false) + end + end + + defmacrop defdomain(resources) do + quote do + Code.compiler_options(ignore_module_conflict: true) + + defmodule Domain do + use Ash.Domain + + resources do + for resource <- unquote(resources) do + resource(resource) + end + end + end + + Code.compiler_options(ignore_module_conflict: false) + end + end + + describe "creating initial snapshots" do + setup do + on_exit(fn -> + File.rm_rf!("test_snapshots_path") + File.rm_rf!("test_migration_path") + end) + + defposts do + sqlite do + migration_types(second_title: {:varchar, 16}) + migration_defaults(title_with_default: "\"fred\"") + end + + identities do + identity(:title, [:title]) + identity(:thing, [:title, :second_title]) + identity(:thing_with_source, [:title, :title_with_source]) + end + + attributes do + uuid_primary_key(:id) + attribute(:title, :string) + attribute(:second_title, :string) + attribute(:title_with_source, :string, source: :t_w_s) + attribute(:title_with_default, :string) + attribute(:email, Test.Support.Types.Email) + end + end + + defdomain([Post]) + + Mix.shell(Mix.Shell.Process) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + :ok + end + + test "the migration sets up resources correctly" do + # the snapshot exists and contains valid json + assert File.read!(Path.wildcard("test_snapshots_path/test_repo/posts/*.json")) + |> Jason.decode!(keys: :atoms!) + + assert [file] = Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") + + file_contents = File.read!(file) + + # the migration creates the table + assert file_contents =~ "create table(:posts, primary_key: false) do" + + # the migration sets up the custom_indexes + assert file_contents =~ + ~S{create index(:posts, ["id"], name: "test_unique_index", unique: true)} + + assert file_contents =~ ~S{create index(:posts, ["id"]} + + # the migration adds the id, with its default + assert file_contents =~ + ~S[add :id, :uuid, null: false, primary_key: true] + + # the migration adds the id, with its default + assert file_contents =~ + ~S[add :title_with_default, :text, default: "fred"] + + # the migration adds other attributes + assert file_contents =~ ~S[add :title, :text] + + # the migration unwraps newtypes + assert file_contents =~ ~S[add :email, :text] + + # the migration adds custom attributes + assert file_contents =~ ~S[add :second_title, :varchar, size: 16] + + # the migration creates unique_indexes based on the identities of the resource + assert file_contents =~ ~S{create unique_index(:posts, [:title], name: "posts_title_index")} + + # the migration creates unique_indexes based on the identities of the resource + assert file_contents =~ + ~S{create unique_index(:posts, [:title, :second_title], name: "posts_thing_index")} + + # the migration creates unique_indexes using the `source` on the attributes of the identity on the resource + assert file_contents =~ + ~S{create unique_index(:posts, [:title, :t_w_s], name: "posts_thing_with_source_index")} + end + end + + describe "creating follow up migrations" do + setup do + on_exit(fn -> + File.rm_rf!("test_snapshots_path") + File.rm_rf!("test_migration_path") + end) + + defposts do + identities do + identity(:title, [:title]) + end + + attributes do + uuid_primary_key(:id) + attribute(:title, :string) + end + end + + defdomain([Post]) + + Mix.shell(Mix.Shell.Process) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + :ok + end + + test "when renaming an index, it is properly renamed" do + defposts do + sqlite do + identity_index_names(title: "titles_r_unique_dawg") + end + + identities do + identity(:title, [:title]) + end + + attributes do + uuid_primary_key(:id) + attribute(:title, :string) + end + end + + defdomain([Post]) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + assert [_file1, file2] = + Enum.sort(Path.wildcard("test_migration_path/**/*_migrate_resources*.exs")) + + assert File.read!(file2) =~ + ~S[ALTER INDEX posts_title_index RENAME TO titles_r_unique_dawg] + end + + test "when adding a field, it adds the field" do + defposts do + identities do + identity(:title, [:title]) + end + + attributes do + uuid_primary_key(:id) + attribute(:title, :string) + attribute(:name, :string, allow_nil?: false) + end + end + + defdomain([Post]) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + assert [_file1, file2] = + Enum.sort(Path.wildcard("test_migration_path/**/*_migrate_resources*.exs")) + + assert File.read!(file2) =~ + ~S[add :name, :text, null: false] + end + + test "when renaming a field, it asks if you are renaming it, and renames it if you are" do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:name, :string, allow_nil?: false) + end + end + + defdomain([Post]) + + send(self(), {:mix_shell_input, :yes?, true}) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + assert [_file1, file2] = + Enum.sort(Path.wildcard("test_migration_path/**/*_migrate_resources*.exs")) + + assert File.read!(file2) =~ ~S[rename table(:posts), :title, to: :name] + end + + test "when renaming a field, it asks if you are renaming it, and adds it if you aren't" do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:name, :string, allow_nil?: false) + end + end + + defdomain([Post]) + + send(self(), {:mix_shell_input, :yes?, false}) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + assert [_file1, file2] = + Enum.sort(Path.wildcard("test_migration_path/**/*_migrate_resources*.exs")) + + assert File.read!(file2) =~ + ~S[add :name, :text, null: false] + end + + test "when renaming a field, it asks which field you are renaming it to, and renames it if you are" do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:name, :string, allow_nil?: false) + attribute(:subject, :string, allow_nil?: false) + end + end + + defdomain([Post]) + + send(self(), {:mix_shell_input, :yes?, true}) + send(self(), {:mix_shell_input, :prompt, "subject"}) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + assert [_file1, file2] = + Enum.sort(Path.wildcard("test_migration_path/**/*_migrate_resources*.exs")) + + # Up migration + assert File.read!(file2) =~ ~S[rename table(:posts), :title, to: :subject] + + # Down migration + assert File.read!(file2) =~ ~S[rename table(:posts), :subject, to: :title] + end + + test "when renaming a field, it asks which field you are renaming it to, and adds it if you arent" do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:name, :string, allow_nil?: false) + attribute(:subject, :string, allow_nil?: false) + end + end + + defdomain([Post]) + + send(self(), {:mix_shell_input, :yes?, false}) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + assert [_file1, file2] = + Enum.sort(Path.wildcard("test_migration_path/**/*_migrate_resources*.exs")) + + assert File.read!(file2) =~ + ~S[add :subject, :text, null: false] + end + + test "when an attribute exists only on some of the resources that use the same table, it isn't marked as null: false" do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:title, :string) + attribute(:example, :string, allow_nil?: false) + end + end + + defposts Post2 do + attributes do + uuid_primary_key(:id) + end + end + + defdomain([Post, Post2]) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + assert [_file1, file2] = + Enum.sort(Path.wildcard("test_migration_path/**/*_migrate_resources*.exs")) + + assert File.read!(file2) =~ + ~S[add :example, :text] <> "\n" + + refute File.read!(file2) =~ ~S[null: false] + end + end + + describe "auto incrementing integer, when generated" do + setup do + on_exit(fn -> + File.rm_rf!("test_snapshots_path") + File.rm_rf!("test_migration_path") + end) + + defposts do + attributes do + attribute(:id, :integer, generated?: true, allow_nil?: false, primary_key?: true) + attribute(:views, :integer) + end + end + + defdomain([Post]) + + Mix.shell(Mix.Shell.Process) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + :ok + end + + test "when an integer is generated and default nil, it is a bigserial" do + assert [file] = Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") + + assert File.read!(file) =~ + ~S[add :id, :bigserial, null: false, primary_key: true] + + assert File.read!(file) =~ + ~S[add :views, :bigint] + end + end + + describe "--check option" do + setup do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:title, :string) + end + end + + defdomain([Post]) + + [domain: Domain] + end + + test "returns code(1) if snapshots and resources don't fit", %{domain: domain} do + assert catch_exit( + AshSqlite.MigrationGenerator.generate(domain, + snapshot_path: "test_snapshot_path", + migration_path: "test_migration_path", + check: true + ) + ) == {:shutdown, 1} + + refute File.exists?(Path.wildcard("test_migration_path2/**/*_migrate_resources*.exs")) + refute File.exists?(Path.wildcard("test_snapshots_path2/test_repo/posts/*.json")) + end + end + + describe "references" do + setup do + on_exit(fn -> + File.rm_rf!("test_snapshots_path") + File.rm_rf!("test_migration_path") + end) + end + + test "references are inferred automatically" do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:title, :string) + attribute(:foobar, :string) + end + end + + defposts Post2 do + attributes do + uuid_primary_key(:id) + attribute(:name, :string) + end + + relationships do + belongs_to(:post, Post) + end + end + + defdomain([Post, Post2]) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + assert [file] = Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") + + assert File.read!(file) =~ + ~S[references(:posts, column: :id, name: "posts_post_id_fkey", type: :uuid)] + end + + test "references are inferred automatically if the attribute has a different type" do + defposts do + attributes do + attribute(:id, :string, primary_key?: true, allow_nil?: false) + attribute(:title, :string) + attribute(:foobar, :string) + end + end + + defposts Post2 do + attributes do + attribute(:id, :string, primary_key?: true, allow_nil?: false) + attribute(:name, :string) + end + + relationships do + belongs_to(:post, Post, attribute_type: :string) + end + end + + defdomain([Post, Post2]) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + assert [file] = Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") + + assert File.read!(file) =~ + ~S[references(:posts, column: :id, name: "posts_post_id_fkey", type: :text)] + end + + test "when modified, the foreign key is dropped before modification" do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:title, :string) + attribute(:foobar, :string) + end + end + + defposts Post2 do + attributes do + uuid_primary_key(:id) + attribute(:name, :string) + end + + relationships do + belongs_to(:post, Post) + end + end + + defdomain([Post, Post2]) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + defposts Post2 do + sqlite do + references do + reference(:post, name: "special_post_fkey", on_delete: :delete, on_update: :update) + end + end + + attributes do + uuid_primary_key(:id) + attribute(:name, :string) + end + + relationships do + belongs_to(:post, Post) + end + end + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + assert file = + "test_migration_path/**/*_migrate_resources*.exs" + |> Path.wildcard() + |> Enum.sort() + |> Enum.at(1) + |> File.read!() + + assert file =~ + ~S[references(:posts, column: :id, name: "special_post_fkey", type: :uuid, on_delete: :delete_all, on_update: :update_all)] + + assert file =~ ~S[drop constraint(:posts, "posts_post_id_fkey")] + + assert [_, down_code] = String.split(file, "def down do") + + assert [_, after_drop] = + String.split(down_code, "drop constraint(:posts, \"special_post_fkey\")") + + assert after_drop =~ ~S[references(:posts] + end + end + + describe "polymorphic resources" do + setup do + on_exit(fn -> + File.rm_rf!("test_snapshots_path") + File.rm_rf!("test_migration_path") + end) + + defmodule Comment do + use Ash.Resource, + domain: nil, + data_layer: AshSqlite.DataLayer + + sqlite do + polymorphic?(true) + repo(AshSqlite.TestRepo) + end + + attributes do + uuid_primary_key(:id) + attribute(:resource_id, :uuid) + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + end + + defmodule Post do + use Ash.Resource, + domain: nil, + data_layer: AshSqlite.DataLayer + + sqlite do + table "posts" + repo(AshSqlite.TestRepo) + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + + attributes do + uuid_primary_key(:id) + end + + relationships do + has_many(:comments, Comment, + destination_attribute: :resource_id, + relationship_context: %{data_layer: %{table: "post_comments"}} + ) + + belongs_to(:best_comment, Comment, + destination_attribute: :id, + relationship_context: %{data_layer: %{table: "post_comments"}} + ) + end + end + + defdomain([Post, Comment]) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + [domain: Domain] + end + + test "it uses the relationship's table context if it is set" do + assert [file] = Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") + + assert File.read!(file) =~ + ~S[references(:post_comments, column: :id, name: "posts_best_comment_id_fkey", type: :uuid)] + end + end + + describe "default values" do + setup do + on_exit(fn -> + File.rm_rf!("test_snapshots_path") + File.rm_rf!("test_migration_path") + end) + end + + test "when default value is specified that has no impl" do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:product_code, :term, default: {"xyz"}) + end + end + + defdomain([Post]) + + capture_log(fn -> + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + end) + + assert [file1] = Enum.sort(Path.wildcard("test_migration_path/**/*_migrate_resources*.exs")) + + file = File.read!(file1) + + assert file =~ + ~S[add :product_code, :binary] + end + end + + describe "follow up with references" do + setup do + on_exit(fn -> + File.rm_rf!("test_snapshots_path") + File.rm_rf!("test_migration_path") + end) + + defposts do + attributes do + uuid_primary_key(:id) + attribute(:title, :string) + end + end + + defmodule Comment do + use Ash.Resource, + domain: nil, + data_layer: AshSqlite.DataLayer + + sqlite do + table "comments" + repo AshSqlite.TestRepo + end + + attributes do + uuid_primary_key(:id) + end + + relationships do + belongs_to(:post, Post) + end + end + + defdomain([Post, Comment]) + + Mix.shell(Mix.Shell.Process) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + :ok + end + + test "when changing the primary key, it changes properly" do + defposts do + attributes do + attribute(:id, :uuid, primary_key?: false, default: &Ecto.UUID.generate/0) + uuid_primary_key(:guid) + attribute(:title, :string) + end + end + + defmodule Comment do + use Ash.Resource, + domain: nil, + data_layer: AshSqlite.DataLayer + + sqlite do + table "comments" + repo AshSqlite.TestRepo + end + + attributes do + uuid_primary_key(:id) + end + + relationships do + belongs_to(:post, Post) + end + end + + defdomain([Post, Comment]) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false + ) + + assert [_file1, file2] = + Enum.sort(Path.wildcard("test_migration_path/**/*_migrate_resources*.exs")) + + file = File.read!(file2) + + assert [before_index_drop, after_index_drop] = + String.split(file, ~S[drop constraint("posts", "posts_pkey")], parts: 2) + + assert before_index_drop =~ ~S[drop constraint(:comments, "comments_post_id_fkey")] + + assert after_index_drop =~ ~S[modify :id, :uuid, null: true, primary_key: false] + + assert after_index_drop =~ + ~S[modify :post_id, references(:posts, column: :id, name: "comments_post_id_fkey", type: :uuid)] + end + end +end diff --git a/test/polymorphism_test.exs b/test/polymorphism_test.exs new file mode 100644 index 0000000..519a0ea --- /dev/null +++ b/test/polymorphism_test.exs @@ -0,0 +1,29 @@ +defmodule AshSqlite.PolymorphismTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.{Post, Rating} + + require Ash.Query + + test "you can create related data" do + Post + |> Ash.Changeset.for_create(:create, rating: %{score: 10}) + |> Ash.create!() + + assert [%{score: 10}] = + Rating + |> Ash.Query.set_context(%{data_layer: %{table: "post_ratings"}}) + |> Ash.read!() + end + + test "you can read related data" do + Post + |> Ash.Changeset.for_create(:create, rating: %{score: 10}) + |> Ash.create!() + + assert [%{score: 10}] = + Post + |> Ash.Query.load(:ratings) + |> Ash.read_one!() + |> Map.get(:ratings) + end +end diff --git a/test/primary_key_test.exs b/test/primary_key_test.exs new file mode 100644 index 0000000..40b5340 --- /dev/null +++ b/test/primary_key_test.exs @@ -0,0 +1,52 @@ +defmodule AshSqlite.Test.PrimaryKeyTest do + @moduledoc false + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.{IntegerPost, Post, PostView} + + require Ash.Query + + test "creates record with integer primary key" do + assert %IntegerPost{} = + IntegerPost |> Ash.Changeset.for_create(:create, %{title: "title"}) |> Ash.create!() + end + + test "creates record with uuid primary key" do + assert %Post{} = Post |> Ash.Changeset.for_create(:create, %{title: "title"}) |> Ash.create!() + end + + describe "resources without a primary key" do + test "records can be created" do + post = + Post + |> Ash.Changeset.for_action(:create, %{title: "not very interesting"}) + |> Ash.create!() + + assert {:ok, view} = + PostView + |> Ash.Changeset.for_action(:create, %{browser: :firefox, post_id: post.id}) + |> Ash.create() + + assert view.browser == :firefox + assert view.post_id == post.id + assert DateTime.diff(DateTime.utc_now(), view.time, :microsecond) < 1_000_000 + end + + test "records can be queried" do + post = + Post + |> Ash.Changeset.for_action(:create, %{title: "not very interesting"}) + |> Ash.create!() + + expected = + PostView + |> Ash.Changeset.for_action(:create, %{browser: :firefox, post_id: post.id}) + |> Ash.create!() + + assert {:ok, [actual]} = Ash.read(PostView) + + assert actual.time == expected.time + assert actual.browser == expected.browser + assert actual.post_id == expected.post_id + end + end +end diff --git a/test/select_test.exs b/test/select_test.exs new file mode 100644 index 0000000..85af50a --- /dev/null +++ b/test/select_test.exs @@ -0,0 +1,15 @@ +defmodule AshSqlite.SelectTest do + @moduledoc false + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.Post + + require Ash.Query + + test "values not selected in the query are not present in the response" do + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + assert [%{title: %Ash.NotLoaded{}}] = Ash.read!(Ash.Query.select(Post, :id)) + end +end diff --git a/test/sort_test.exs b/test/sort_test.exs new file mode 100644 index 0000000..c17f4b3 --- /dev/null +++ b/test/sort_test.exs @@ -0,0 +1,175 @@ +defmodule AshSqlite.SortTest do + @moduledoc false + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.{Comment, Post, PostLink} + + require Ash.Query + + test "multi-column sorts work" do + Post + |> Ash.Changeset.for_create(:create, %{title: "aaa", score: 0}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "aaa", score: 1}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "bbb", score: 0}) + |> Ash.create!() + + assert [ + %{title: "aaa", score: 0}, + %{title: "aaa", score: 1}, + %{title: "bbb"} + ] = + Ash.read!( + Post + |> Ash.Query.sort(title: :asc, score: :asc) + ) + end + + test "multi-column sorts work on inclusion" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "aaa", score: 0}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "aaa", score: 1}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "bbb", score: 0}) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "aaa", likes: 1}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "bbb", likes: 1}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + Comment + |> Ash.Changeset.for_create(:create, %{title: "aaa", likes: 2}) + |> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove) + |> Ash.create!() + + posts = + Post + |> Ash.Query.load( + comments: + Comment + |> Ash.Query.sort([:title, :likes]) + |> Ash.Query.select([:title, :likes]) + |> Ash.Query.limit(1) + ) + |> Ash.Query.sort([:title, :score]) + |> Ash.read!() + + assert [ + %{title: "aaa", comments: [%{title: "aaa"}]}, + %{title: "aaa"}, + %{title: "bbb"} + ] = posts + end + + test "multicolumn sort works with a select statement" do + Post + |> Ash.Changeset.for_create(:create, %{title: "aaa", score: 0}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "aaa", score: 1}) + |> Ash.create!() + + Post + |> Ash.Changeset.for_create(:create, %{title: "bbb", score: 0}) + |> Ash.create!() + + assert [ + %{title: "aaa", score: 0}, + %{title: "aaa", score: 1}, + %{title: "bbb"} + ] = + Ash.read!( + Post + |> Ash.Query.sort(title: :asc, score: :asc) + |> Ash.Query.select([:title, :score]) + ) + end + + test "sorting when joining to a many to many relationship sorts properly" do + post1 = + Post + |> Ash.Changeset.for_create(:create, %{title: "aaa", score: 0}) + |> Ash.create!() + + post2 = + Post + |> Ash.Changeset.for_create(:create, %{title: "bbb", score: 1}) + |> Ash.create!() + + post3 = + Post + |> Ash.Changeset.for_create(:create, %{title: "ccc", score: 0}) + |> Ash.create!() + + PostLink + |> Ash.Changeset.new() + |> Ash.Changeset.manage_relationship(:source_post, post1, type: :append) + |> Ash.Changeset.manage_relationship(:destination_post, post3, type: :append) + |> Ash.create!() + + PostLink + |> Ash.Changeset.new() + |> Ash.Changeset.manage_relationship(:source_post, post2, type: :append) + |> Ash.Changeset.manage_relationship(:destination_post, post2, type: :append) + |> Ash.create!() + + PostLink + |> Ash.Changeset.new() + |> Ash.Changeset.manage_relationship(:source_post, post3, type: :append) + |> Ash.Changeset.manage_relationship(:destination_post, post1, type: :append) + |> Ash.create!() + + assert [ + %{title: "aaa"}, + %{title: "bbb"}, + %{title: "ccc"} + ] = + Ash.read!( + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.filter(linked_posts.title in ["aaa", "bbb", "ccc"]) + ) + + assert [ + %{title: "ccc"}, + %{title: "bbb"}, + %{title: "aaa"} + ] = + Ash.read!( + Post + |> Ash.Query.sort(title: :desc) + |> Ash.Query.filter(linked_posts.title in ["aaa", "bbb", "ccc"] or title == "aaa") + ) + + assert [ + %{title: "ccc"}, + %{title: "bbb"}, + %{title: "aaa"} + ] = + Ash.read!( + Post + |> Ash.Query.sort(title: :desc) + |> Ash.Query.filter( + linked_posts.title in ["aaa", "bbb", "ccc"] or + post_links.source_post_id == ^post2.id + ) + ) + end +end diff --git a/test/support/concat.ex b/test/support/concat.ex new file mode 100644 index 0000000..0977a28 --- /dev/null +++ b/test/support/concat.ex @@ -0,0 +1,35 @@ +defmodule AshSqlite.Test.Concat do + @moduledoc false + use Ash.Resource.Calculation + require Ash.Query + + def init(opts) do + if opts[:keys] && is_list(opts[:keys]) && Enum.all?(opts[:keys], &is_atom/1) do + {:ok, opts} + else + {:error, "Expected a `keys` option for which keys to concat"} + end + end + + def expression(opts, %{arguments: %{separator: separator}}) do + Enum.reduce(opts[:keys], nil, fn key, expr -> + if expr do + if separator do + expr(^expr <> ^separator <> ^ref(key)) + else + expr(^expr <> ^ref(key)) + end + else + expr(^ref(key)) + end + end) + end + + def calculate(records, opts, %{separator: separator}) do + Enum.map(records, fn record -> + Enum.map_join(opts[:keys], separator, fn key -> + to_string(Map.get(record, key)) + end) + end) + end +end diff --git a/test/support/domain.ex b/test/support/domain.ex new file mode 100644 index 0000000..90c0680 --- /dev/null +++ b/test/support/domain.ex @@ -0,0 +1,23 @@ +defmodule AshSqlite.Test.Domain do + @moduledoc false + use Ash.Domain + + resources do + resource(AshSqlite.Test.Post) + resource(AshSqlite.Test.Comment) + resource(AshSqlite.Test.IntegerPost) + resource(AshSqlite.Test.Rating) + resource(AshSqlite.Test.PostLink) + resource(AshSqlite.Test.PostView) + resource(AshSqlite.Test.Author) + resource(AshSqlite.Test.Profile) + resource(AshSqlite.Test.User) + resource(AshSqlite.Test.Account) + resource(AshSqlite.Test.Organization) + resource(AshSqlite.Test.Manager) + end + + authorization do + authorize(:when_requested) + end +end diff --git a/test/support/relationships/comments_containing_title.ex b/test/support/relationships/comments_containing_title.ex new file mode 100644 index 0000000..d80d049 --- /dev/null +++ b/test/support/relationships/comments_containing_title.ex @@ -0,0 +1,48 @@ +defmodule AshSqlite.Test.Post.CommentsContainingTitle do + @moduledoc false + + use Ash.Resource.ManualRelationship + use AshSqlite.ManualRelationship + require Ash.Query + require Ecto.Query + + def load(posts, _opts, %{query: query, actor: actor, authorize?: authorize?}) do + post_ids = Enum.map(posts, & &1.id) + + {:ok, + query + |> Ash.Query.filter(post_id in ^post_ids) + |> Ash.Query.filter(contains(title, post.title)) + |> Ash.read!(actor: actor, authorize?: authorize?) + |> Enum.group_by(& &1.post_id)} + end + + def ash_sqlite_join(query, _opts, current_binding, as_binding, :inner, destination_query) do + {:ok, + Ecto.Query.from(_ in query, + join: dest in ^destination_query, + as: ^as_binding, + on: dest.post_id == as(^current_binding).id, + on: fragment("instr(?, ?) > 0", dest.title, as(^current_binding).title) + )} + end + + def ash_sqlite_join(query, _opts, current_binding, as_binding, :left, destination_query) do + {:ok, + Ecto.Query.from(_ in query, + left_join: dest in ^destination_query, + as: ^as_binding, + on: dest.post_id == as(^current_binding).id, + on: fragment("instr(?, ?) > 0", dest.title, as(^current_binding).title) + )} + end + + def ash_sqlite_subquery(_opts, current_binding, as_binding, destination_query) do + {:ok, + Ecto.Query.from(_ in destination_query, + where: parent_as(^current_binding).id == as(^as_binding).post_id, + where: + fragment("instr(?, ?) > 0", as(^as_binding).title, parent_as(^current_binding).title) + )} + end +end diff --git a/test/support/repo_case.ex b/test/support/repo_case.ex new file mode 100644 index 0000000..a405788 --- /dev/null +++ b/test/support/repo_case.ex @@ -0,0 +1,28 @@ +defmodule AshSqlite.RepoCase do + @moduledoc false + use ExUnit.CaseTemplate + + alias Ecto.Adapters.SQL.Sandbox + + using do + quote do + alias AshSqlite.TestRepo + + import Ecto + import Ecto.Query + import AshSqlite.RepoCase + + # and any other stuff + end + end + + setup tags do + :ok = Sandbox.checkout(AshSqlite.TestRepo) + + unless tags[:async] do + Sandbox.mode(AshSqlite.TestRepo, {:shared, self()}) + end + + :ok + end +end diff --git a/test/support/resources/account.ex b/test/support/resources/account.ex new file mode 100644 index 0000000..92903ce --- /dev/null +++ b/test/support/resources/account.ex @@ -0,0 +1,32 @@ +defmodule AshSqlite.Test.Account do + @moduledoc false + use Ash.Resource, domain: AshSqlite.Test.Domain, data_layer: AshSqlite.DataLayer + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + attributes do + uuid_primary_key(:id) + attribute(:is_active, :boolean, public?: true) + end + + calculations do + calculate( + :active, + :boolean, + expr(is_active), + public?: true + ) + end + + sqlite do + table "accounts" + repo(AshSqlite.TestRepo) + end + + relationships do + belongs_to(:user, AshSqlite.Test.User, public?: true) + end +end diff --git a/test/support/resources/author.ex b/test/support/resources/author.ex new file mode 100644 index 0000000..df6c88d --- /dev/null +++ b/test/support/resources/author.ex @@ -0,0 +1,74 @@ +defmodule AshSqlite.Test.Author do + @moduledoc false + use Ash.Resource, + domain: AshSqlite.Test.Domain, + data_layer: AshSqlite.DataLayer + + sqlite do + table("authors") + repo(AshSqlite.TestRepo) + end + + attributes do + uuid_primary_key(:id, writable?: true) + attribute(:first_name, :string, public?: true) + attribute(:last_name, :string, public?: true) + attribute(:bio, AshSqlite.Test.Bio, public?: true) + #attribute(:badges, {:array, :atom}, public?: true) + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + relationships do + has_one(:profile, AshSqlite.Test.Profile, public?: true) + has_many(:posts, AshSqlite.Test.Post, public?: true) + end + + calculations do + calculate(:title, :string, expr(bio[:title])) + calculate(:full_name, :string, expr(first_name <> " " <> last_name)) + # calculate(:full_name_with_nils, :string, expr(string_join([first_name, last_name], " "))) + # calculate(:full_name_with_nils_no_joiner, :string, expr(string_join([first_name, last_name]))) + # calculate(:split_full_name, {:array, :string}, expr(string_split(full_name))) + + calculate(:first_name_or_bob, :string, expr(first_name || "bob")) + calculate(:first_name_and_bob, :string, expr(first_name && "bob")) + + calculate( + :conditional_full_name, + :string, + expr( + if( + is_nil(first_name) or is_nil(last_name), + "(none)", + first_name <> " " <> last_name + ) + ) + ) + + calculate( + :nested_conditional, + :string, + expr( + if( + is_nil(first_name), + "No First Name", + if( + is_nil(last_name), + "No Last Name", + first_name <> " " <> last_name + ) + ) + ) + ) + + calculate :param_full_name, + :string, + {AshSqlite.Test.Concat, keys: [:first_name, :last_name]} do + argument(:separator, :string, default: " ", constraints: [allow_empty?: true, trim?: false]) + end + end +end diff --git a/test/support/resources/bio.ex b/test/support/resources/bio.ex new file mode 100644 index 0000000..ce87602 --- /dev/null +++ b/test/support/resources/bio.ex @@ -0,0 +1,21 @@ +defmodule AshSqlite.Test.Bio do + @moduledoc false + use Ash.Resource, data_layer: :embedded + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + attributes do + attribute(:title, :string, public?: true) + attribute(:bio, :string, public?: true) + attribute(:years_of_experience, :integer, public?: true) + + attribute :list_of_strings, {:array, :string} do + public?(true) + allow_nil?(true) + default(nil) + end + end +end diff --git a/test/support/resources/comment.ex b/test/support/resources/comment.ex new file mode 100644 index 0000000..7c6e2fb --- /dev/null +++ b/test/support/resources/comment.ex @@ -0,0 +1,63 @@ +defmodule AshSqlite.Test.Comment do + @moduledoc false + use Ash.Resource, + domain: AshSqlite.Test.Domain, + data_layer: AshSqlite.DataLayer, + authorizers: [ + Ash.Policy.Authorizer + ] + + policies do + bypass action_type(:read) do + # Check that the comment is in the same org (via post) as actor + authorize_if(relates_to_actor_via([:post, :organization, :users])) + end + end + + sqlite do + table "comments" + repo(AshSqlite.TestRepo) + + references do + reference(:post, on_delete: :delete, on_update: :update, name: "special_name_fkey") + end + end + + actions do + default_accept(:*) + defaults([:read, :update, :destroy]) + + create :create do + primary?(true) + argument(:rating, :map) + + change(manage_relationship(:rating, :ratings, on_missing: :ignore, on_match: :create)) + end + end + + attributes do + uuid_primary_key(:id) + attribute(:title, :string, public?: true) + attribute(:likes, :integer, public?: true) + attribute(:arbitrary_timestamp, :utc_datetime_usec, public?: true) + create_timestamp(:created_at, writable?: true, public?: true) + end + + relationships do + belongs_to(:post, AshSqlite.Test.Post, public?: true) + belongs_to(:author, AshSqlite.Test.Author, public?: true) + + has_many(:ratings, AshSqlite.Test.Rating, + public?: true, + destination_attribute: :resource_id, + relationship_context: %{data_layer: %{table: "comment_ratings"}} + ) + + has_many(:popular_ratings, AshSqlite.Test.Rating, + public?: true, + destination_attribute: :resource_id, + relationship_context: %{data_layer: %{table: "comment_ratings"}}, + filter: expr(score > 5) + ) + end +end diff --git a/test/support/resources/integer_post.ex b/test/support/resources/integer_post.ex new file mode 100644 index 0000000..874bfa3 --- /dev/null +++ b/test/support/resources/integer_post.ex @@ -0,0 +1,21 @@ +defmodule AshSqlite.Test.IntegerPost do + @moduledoc false + use Ash.Resource, + domain: AshSqlite.Test.Domain, + data_layer: AshSqlite.DataLayer + + sqlite do + table "integer_posts" + repo AshSqlite.TestRepo + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + attributes do + integer_primary_key(:id) + attribute(:title, :string, public?: true) + end +end diff --git a/test/support/resources/manager.ex b/test/support/resources/manager.ex new file mode 100644 index 0000000..725f596 --- /dev/null +++ b/test/support/resources/manager.ex @@ -0,0 +1,42 @@ +defmodule AshSqlite.Test.Manager do + @moduledoc false + use Ash.Resource, + domain: AshSqlite.Test.Domain, + data_layer: AshSqlite.DataLayer + + sqlite do + table("managers") + repo(AshSqlite.TestRepo) + end + + actions do + default_accept(:*) + defaults([:read, :update, :destroy]) + + create :create do + primary?(true) + argument(:organization_id, :uuid, allow_nil?: false) + + change(manage_relationship(:organization_id, :organization, type: :append_and_remove)) + end + end + + identities do + identity(:uniq_code, :code) + end + + attributes do + uuid_primary_key(:id) + attribute(:name, :string, public?: true) + attribute(:code, :string, allow_nil?: false, public?: true) + attribute(:must_be_present, :string, allow_nil?: false, public?: true) + attribute(:role, :string, public?: true) + end + + relationships do + belongs_to :organization, AshSqlite.Test.Organization do + public?(true) + attribute_writable?(true) + end + end +end diff --git a/test/support/resources/organization.ex b/test/support/resources/organization.ex new file mode 100644 index 0000000..f2a1524 --- /dev/null +++ b/test/support/resources/organization.ex @@ -0,0 +1,21 @@ +defmodule AshSqlite.Test.Organization do + @moduledoc false + use Ash.Resource, + domain: AshSqlite.Test.Domain, + data_layer: AshSqlite.DataLayer + + sqlite do + table("orgs") + repo(AshSqlite.TestRepo) + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + attributes do + uuid_primary_key(:id, writable?: true) + attribute(:name, :string, public?: true) + end +end diff --git a/test/support/resources/post.ex b/test/support/resources/post.ex new file mode 100644 index 0000000..968e121 --- /dev/null +++ b/test/support/resources/post.ex @@ -0,0 +1,235 @@ +defmodule AshSqlite.Test.Post do + @moduledoc false + use Ash.Resource, + domain: AshSqlite.Test.Domain, + data_layer: AshSqlite.DataLayer, + authorizers: [ + Ash.Policy.Authorizer + ] + + policies do + bypass action_type(:read) do + # Check that the post is in the same org as actor + authorize_if(relates_to_actor_via([:organization, :users])) + end + end + + sqlite do + table("posts") + repo(AshSqlite.TestRepo) + base_filter_sql("type = 'sponsored'") + + custom_indexes do + index([:uniq_custom_one, :uniq_custom_two], + unique: true, + message: "dude what the heck" + ) + end + end + + resource do + base_filter(expr(type == type(:sponsored, ^Ash.Type.Atom))) + end + + actions do + default_accept(:*) + defaults([:update, :destroy]) + + read :read do + primary?(true) + end + + read :paginated do + pagination(offset?: true, required?: true) + end + + create :create do + primary?(true) + argument(:rating, :map) + + change( + manage_relationship(:rating, :ratings, + on_missing: :ignore, + on_no_match: :create, + on_match: :create + ) + ) + end + + update :increment_score do + argument(:amount, :integer, default: 1) + change(atomic_update(:score, expr((score || 0) + ^arg(:amount)))) + end + end + + identities do + identity(:uniq_one_and_two, [:uniq_one, :uniq_two]) + end + + attributes do + uuid_primary_key(:id, writable?: true) + attribute(:title, :string, public?: true) + attribute(:score, :integer, public?: true) + attribute(:public, :boolean, public?: true) + attribute(:category, :ci_string, public?: true) + attribute(:type, :atom, default: :sponsored, writable?: false) + attribute(:price, :integer, public?: true) + attribute(:decimal, :decimal, default: Decimal.new(0), public?: true) + attribute(:status, AshSqlite.Test.Types.Status, public?: true) + attribute(:status_enum, AshSqlite.Test.Types.StatusEnum, public?: true) + + attribute(:status_enum_no_cast, AshSqlite.Test.Types.StatusEnumNoCast, + source: :status_enum, + public?: true + ) + + attribute(:stuff, :map, public?: true) + attribute(:uniq_one, :string, public?: true) + attribute(:uniq_two, :string, public?: true) + attribute(:uniq_custom_one, :string, public?: true) + attribute(:uniq_custom_two, :string, public?: true) + create_timestamp(:created_at) + update_timestamp(:updated_at) + end + + code_interface do + define(:get_by_id, action: :read, get_by: [:id]) + define(:increment_score, args: [{:optional, :amount}]) + end + + relationships do + belongs_to :organization, AshSqlite.Test.Organization do + public?(true) + attribute_writable?(true) + end + + belongs_to(:author, AshSqlite.Test.Author, public?: true) + + has_many(:comments, AshSqlite.Test.Comment, destination_attribute: :post_id, public?: true) + + has_many :comments_matching_post_title, AshSqlite.Test.Comment do + public?(true) + filter(expr(title == parent_expr(title))) + end + + has_many :popular_comments, AshSqlite.Test.Comment do + public?(true) + destination_attribute(:post_id) + filter(expr(likes > 10)) + end + + has_many :comments_containing_title, AshSqlite.Test.Comment do + public?(true) + manual(AshSqlite.Test.Post.CommentsContainingTitle) + end + + has_many(:ratings, AshSqlite.Test.Rating, + public?: true, + destination_attribute: :resource_id, + relationship_context: %{data_layer: %{table: "post_ratings"}} + ) + + has_many(:post_links, AshSqlite.Test.PostLink, + public?: true, + destination_attribute: :source_post_id, + filter: [state: :active] + ) + + many_to_many(:linked_posts, __MODULE__, + public?: true, + through: AshSqlite.Test.PostLink, + join_relationship: :post_links, + source_attribute_on_join_resource: :source_post_id, + destination_attribute_on_join_resource: :destination_post_id + ) + + has_many(:views, AshSqlite.Test.PostView, public?: true) + end + + validations do + validate(attribute_does_not_equal(:title, "not allowed")) + end + + calculations do + calculate(:score_after_winning, :integer, expr((score || 0) + 1)) + calculate(:negative_score, :integer, expr(-score)) + calculate(:category_label, :string, expr("(" <> category <> ")")) + calculate(:score_with_score, :string, expr(score <> score)) + calculate(:foo_bar_from_stuff, :string, expr(stuff[:foo][:bar])) + + calculate( + :score_map, + :map, + expr(%{ + negative_score: %{foo: negative_score, bar: negative_score} + }) + ) + + calculate( + :calc_returning_json, + AshSqlite.Test.Money, + expr( + fragment(""" + '{"amount":100, "currency": "usd"}' + """) + ) + ) + + calculate( + :was_created_in_the_last_month, + :boolean, + expr( + # This is written in a silly way on purpose, to test a regression + if( + fragment("(? <= (DATE(? - '+1 month')))", now(), created_at), + true, + false + ) + ) + ) + + calculate( + :price_string, + :string, + CalculatePostPriceString + ) + + calculate( + :price_string_with_currency_sign, + :string, + CalculatePostPriceStringWithSymbol + ) + end +end + +defmodule CalculatePostPriceString do + @moduledoc false + use Ash.Resource.Calculation + + @impl true + def load(_, _, _), do: [:price] + + @impl true + def calculate(records, _, _) do + Enum.map(records, fn %{price: price} -> + dollars = div(price, 100) + cents = rem(price, 100) + "#{dollars}.#{cents}" + end) + end +end + +defmodule CalculatePostPriceStringWithSymbol do + @moduledoc false + use Ash.Resource.Calculation + + @impl true + def load(_, _, _), do: [:price_string] + + @impl true + def calculate(records, _, _) do + Enum.map(records, fn %{price_string: price_string} -> + "#{price_string}$" + end) + end +end diff --git a/test/support/resources/post_link.ex b/test/support/resources/post_link.ex new file mode 100644 index 0000000..a794d73 --- /dev/null +++ b/test/support/resources/post_link.ex @@ -0,0 +1,42 @@ +defmodule AshSqlite.Test.PostLink do + @moduledoc false + use Ash.Resource, + domain: AshSqlite.Test.Domain, + data_layer: AshSqlite.DataLayer + + sqlite do + table "post_links" + repo AshSqlite.TestRepo + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + identities do + identity(:unique_link, [:source_post_id, :destination_post_id]) + end + + attributes do + attribute :state, :atom do + public?(true) + constraints(one_of: [:active, :archived]) + default(:active) + end + end + + relationships do + belongs_to :source_post, AshSqlite.Test.Post do + public?(true) + allow_nil?(false) + primary_key?(true) + end + + belongs_to :destination_post, AshSqlite.Test.Post do + public?(true) + allow_nil?(false) + primary_key?(true) + end + end +end diff --git a/test/support/resources/post_views.ex b/test/support/resources/post_views.ex new file mode 100644 index 0000000..c87307a --- /dev/null +++ b/test/support/resources/post_views.ex @@ -0,0 +1,35 @@ +defmodule AshSqlite.Test.PostView do + @moduledoc false + use Ash.Resource, domain: AshSqlite.Test.Domain, data_layer: AshSqlite.DataLayer + + actions do + default_accept(:*) + defaults([:create, :read]) + end + + attributes do + create_timestamp(:time) + attribute(:browser, :atom, constraints: [one_of: [:firefox, :chrome, :edge]], public?: true) + end + + relationships do + belongs_to :post, AshSqlite.Test.Post do + public?(true) + allow_nil?(false) + attribute_writable?(true) + end + end + + resource do + require_primary_key?(false) + end + + sqlite do + table "post_views" + repo AshSqlite.TestRepo + + references do + reference :post, ignore?: true + end + end +end diff --git a/test/support/resources/profile.ex b/test/support/resources/profile.ex new file mode 100644 index 0000000..043a91a --- /dev/null +++ b/test/support/resources/profile.ex @@ -0,0 +1,25 @@ +defmodule AshSqlite.Test.Profile do + @moduledoc false + use Ash.Resource, + domain: AshSqlite.Test.Domain, + data_layer: AshSqlite.DataLayer + + sqlite do + table("profile") + repo(AshSqlite.TestRepo) + end + + attributes do + uuid_primary_key(:id, writable?: true) + attribute(:description, :string, public?: true) + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + relationships do + belongs_to(:author, AshSqlite.Test.Author, public?: true) + end +end diff --git a/test/support/resources/rating.ex b/test/support/resources/rating.ex new file mode 100644 index 0000000..90f5760 --- /dev/null +++ b/test/support/resources/rating.ex @@ -0,0 +1,22 @@ +defmodule AshSqlite.Test.Rating do + @moduledoc false + use Ash.Resource, + domain: AshSqlite.Test.Domain, + data_layer: AshSqlite.DataLayer + + sqlite do + polymorphic?(true) + repo AshSqlite.TestRepo + end + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + attributes do + uuid_primary_key(:id) + attribute(:score, :integer, public?: true) + attribute(:resource_id, :uuid, public?: true) + end +end diff --git a/test/support/resources/user.ex b/test/support/resources/user.ex new file mode 100644 index 0000000..7baab1c --- /dev/null +++ b/test/support/resources/user.ex @@ -0,0 +1,24 @@ +defmodule AshSqlite.Test.User do + @moduledoc false + use Ash.Resource, domain: AshSqlite.Test.Domain, data_layer: AshSqlite.DataLayer + + actions do + default_accept(:*) + defaults([:create, :read, :update, :destroy]) + end + + attributes do + uuid_primary_key(:id) + attribute(:is_active, :boolean, public?: true) + end + + sqlite do + table "users" + repo(AshSqlite.TestRepo) + end + + relationships do + belongs_to(:organization, AshSqlite.Test.Organization, public?: true) + has_many(:accounts, AshSqlite.Test.Account, public?: true) + end +end diff --git a/test/support/test_app.ex b/test/support/test_app.ex new file mode 100644 index 0000000..e074614 --- /dev/null +++ b/test/support/test_app.ex @@ -0,0 +1,13 @@ +defmodule AshSqlite.TestApp do + @moduledoc false + def start(_type, _args) do + children = [ + AshSqlite.TestRepo + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: AshSqlite.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/test/support/test_custom_extension.ex b/test/support/test_custom_extension.ex new file mode 100644 index 0000000..a854a4e --- /dev/null +++ b/test/support/test_custom_extension.ex @@ -0,0 +1,38 @@ +defmodule AshSqlite.TestCustomExtension do + @moduledoc false + + use AshSqlite.CustomExtension, name: "demo-functions", latest_version: 1 + + @impl true + def install(0) do + """ + execute(\"\"\" + CREATE OR REPLACE FUNCTION ash_demo_functions() + RETURNS boolean AS $$ SELECT TRUE $$ + LANGUAGE SQL + IMMUTABLE; + \"\"\") + """ + end + + @impl true + def install(1) do + """ + execute(\"\"\" + CREATE OR REPLACE FUNCTION ash_demo_functions() + RETURNS boolean AS $$ SELECT FALSE $$ + LANGUAGE SQL + IMMUTABLE; + \"\"\") + """ + end + + @impl true + def uninstall(_version) do + """ + execute(\"\"\" + DROP FUNCTION IF EXISTS ash_demo_functions() + \"\"\") + """ + end +end diff --git a/test/support/test_repo.ex b/test/support/test_repo.ex new file mode 100644 index 0000000..fb51b46 --- /dev/null +++ b/test/support/test_repo.ex @@ -0,0 +1,5 @@ +defmodule AshSqlite.TestRepo do + @moduledoc false + use AshSqlite.Repo, + otp_app: :ash_sqlite +end diff --git a/test/support/types/email.ex b/test/support/types/email.ex new file mode 100644 index 0000000..f9fa483 --- /dev/null +++ b/test/support/types/email.ex @@ -0,0 +1,8 @@ +defmodule Test.Support.Types.Email do + @moduledoc false + use Ash.Type.NewType, + subtype_of: :string, + constraints: [ + casing: :lower + ] +end diff --git a/test/support/types/money.ex b/test/support/types/money.ex new file mode 100644 index 0000000..d576d6b --- /dev/null +++ b/test/support/types/money.ex @@ -0,0 +1,18 @@ +defmodule AshSqlite.Test.Money do + @moduledoc false + use Ash.Resource, + data_layer: :embedded + + attributes do + attribute :amount, :integer do + public?(true) + allow_nil?(false) + constraints(min: 0) + end + + attribute :currency, :atom do + public?(true) + constraints(one_of: [:eur, :usd]) + end + end +end diff --git a/test/support/types/status.ex b/test/support/types/status.ex new file mode 100644 index 0000000..38f422f --- /dev/null +++ b/test/support/types/status.ex @@ -0,0 +1,6 @@ +defmodule AshSqlite.Test.Types.Status do + @moduledoc false + use Ash.Type.Enum, values: [:open, :closed] + + def storage_type, do: :string +end diff --git a/test/support/types/status_enum.ex b/test/support/types/status_enum.ex new file mode 100644 index 0000000..e95a7c8 --- /dev/null +++ b/test/support/types/status_enum.ex @@ -0,0 +1,6 @@ +defmodule AshSqlite.Test.Types.StatusEnum do + @moduledoc false + use Ash.Type.Enum, values: [:open, :closed] + + def storage_type, do: :status +end diff --git a/test/support/types/status_enum_no_cast.ex b/test/support/types/status_enum_no_cast.ex new file mode 100644 index 0000000..2cd9974 --- /dev/null +++ b/test/support/types/status_enum_no_cast.ex @@ -0,0 +1,8 @@ +defmodule AshSqlite.Test.Types.StatusEnumNoCast do + @moduledoc false + use Ash.Type.Enum, values: [:open, :closed] + + def storage_type, do: :status + + def cast_in_query?, do: false +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..5329339 --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1,6 @@ +ExUnit.start() +ExUnit.configure(stacktrace_depth: 100) + +AshSqlite.TestRepo.start_link() + +Ecto.Adapters.SQL.Sandbox.mode(AshSqlite.TestRepo, :manual) diff --git a/test/type_test.exs b/test/type_test.exs new file mode 100644 index 0000000..815eb4a --- /dev/null +++ b/test/type_test.exs @@ -0,0 +1,14 @@ +defmodule AshSqlite.Test.TypeTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.Post + + require Ash.Query + + test "uuids can be used as strings in fragments" do + uuid = Ash.UUID.generate() + + Post + |> Ash.Query.filter(fragment("? = ?", id, type(^uuid, :uuid))) + |> Ash.read!() + end +end diff --git a/test/unique_identity_test.exs b/test/unique_identity_test.exs new file mode 100644 index 0000000..6ef6d54 --- /dev/null +++ b/test/unique_identity_test.exs @@ -0,0 +1,45 @@ +defmodule AshSqlite.Test.UniqueIdentityTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.Post + + require Ash.Query + + test "unique constraint errors are properly caught" do + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "title"}) + |> Ash.create!() + + assert_raise Ash.Error.Invalid, + ~r/Invalid value provided for id: has already been taken/, + fn -> + Post + |> Ash.Changeset.for_create(:create, %{id: post.id}) + |> Ash.create!() + end + end + + test "a unique constraint can be used to upsert when the resource has a base filter" do + post = + Post + |> Ash.Changeset.for_create(:create, %{ + title: "title", + uniq_one: "fred", + uniq_two: "astair", + price: 10 + }) + |> Ash.create!() + + new_post = + Post + |> Ash.Changeset.for_create(:create, %{ + title: "title2", + uniq_one: "fred", + uniq_two: "astair" + }) + |> Ash.create!(upsert?: true, upsert_identity: :uniq_one_and_two) + + assert new_post.id == post.id + assert new_post.price == 10 + end +end diff --git a/test/update_test.exs b/test/update_test.exs new file mode 100644 index 0000000..a9e81b8 --- /dev/null +++ b/test/update_test.exs @@ -0,0 +1,46 @@ +defmodule AshSqlite.Test.UpdateTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.Post + + require Ash.Query + + test "updating a record when multiple records are in the table will only update the desired record" do + # This test is here because of a previous bug in update that caused + # all records in the table to be updated. + id_1 = Ash.UUID.generate() + id_2 = Ash.UUID.generate() + + new_post_1 = + Post + |> Ash.Changeset.for_create(:create, %{ + id: id_1, + title: "new_post_1" + }) + |> Ash.create!() + + _new_post_2 = + Post + |> Ash.Changeset.for_create(:create, %{ + id: id_2, + title: "new_post_2" + }) + |> Ash.create!() + + {:ok, updated_post_1} = + new_post_1 + |> Ash.Changeset.for_update(:update, %{ + title: "new_post_1_updated" + }) + |> Ash.update() + + # It is deliberate that post 2 is re-fetched from the db after the + # update to post 1. This ensure that post 2 was not updated. + post_2 = Ash.get!(Post, id_2) + + assert updated_post_1.id == id_1 + assert updated_post_1.title == "new_post_1_updated" + + assert post_2.id == id_2 + assert post_2.title == "new_post_2" + end +end diff --git a/test/upsert_test.exs b/test/upsert_test.exs new file mode 100644 index 0000000..cde27e8 --- /dev/null +++ b/test/upsert_test.exs @@ -0,0 +1,60 @@ +defmodule AshSqlite.Test.UpsertTest do + use AshSqlite.RepoCase, async: false + alias AshSqlite.Test.Post + + require Ash.Query + + test "upserting results in the same created_at timestamp, but a new updated_at timestamp" do + id = Ash.UUID.generate() + + new_post = + Post + |> Ash.Changeset.for_create(:create, %{ + id: id, + title: "title2" + }) + |> Ash.create!(upsert?: true) + + assert new_post.id == id + assert new_post.created_at == new_post.updated_at + + updated_post = + Post + |> Ash.Changeset.for_create(:create, %{ + id: id, + title: "title2" + }) + |> Ash.create!(upsert?: true) + + assert updated_post.id == id + assert updated_post.created_at == new_post.created_at + assert updated_post.created_at != updated_post.updated_at + end + + test "upserting a field with a default sets to the new value" do + id = Ash.UUID.generate() + + new_post = + Post + |> Ash.Changeset.for_create(:create, %{ + id: id, + title: "title2" + }) + |> Ash.create!(upsert?: true) + + assert new_post.id == id + assert new_post.created_at == new_post.updated_at + + updated_post = + Post + |> Ash.Changeset.for_create(:create, %{ + id: id, + title: "title2", + decimal: Decimal.new(5) + }) + |> Ash.create!(upsert?: true) + + assert updated_post.id == id + assert Decimal.equal?(updated_post.decimal, Decimal.new(5)) + end +end