diff --git a/.gitignore b/.gitignore index e79954f..c0a2d6b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,6 @@ ash_sqlite-*.tar test_migration_path test_snapshots_path -test/test.db -test/test.db-shm -test/test.db-wal +test/*test.db +test/*test.db-shm +test/*test.db-wal diff --git a/config/config.exs b/config/config.exs index 7fd1cae..b8d1cf7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -28,8 +28,15 @@ if Mix.env() == :test do pool: Ecto.Adapters.SQL.Sandbox, migration_primary_key: [name: :id, type: :binary_id] + config :ash_sqlite, AshSqlite.TransactingRepo, + database: Path.join(__DIR__, "../test/transacting_test.db"), + pool_size: 1, + migration_lock: false, + pool: Ecto.Adapters.SQL.Sandbox, + migration_primary_key: [name: :id, type: :binary_id] + config :ash_sqlite, - ecto_repos: [AshSqlite.TestRepo], + ecto_repos: [AshSqlite.TestRepo, AshSqlite.TransactingRepo], ash_domains: [ AshSqlite.Test.Domain ] diff --git a/lib/data_layer.ex b/lib/data_layer.ex index a715fc5..79f79d5 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -413,7 +413,14 @@ defmodule AshSqlite.DataLayer do def can?(_, :bulk_create), do: true def can?(_, {:lock, _}), do: false - def can?(_, :transact), do: false + # def can?(_, :transact), do: false + + def can?(resource, :transact) do + repo = AshSqlite.DataLayer.Info.repo(resource) + + repo.config()[:transactions_enabled?] + end + def can?(_, :composite_primary_key), do: true def can?(_, {:atomic, :update}), do: true def can?(_, {:atomic, :upsert}), do: true diff --git a/lib/repo.ex b/lib/repo.ex index c30ad79..f303ed4 100644 --- a/lib/repo.ex +++ b/lib/repo.ex @@ -16,6 +16,7 @@ defmodule AshSqlite.Repo do - `:tenant_migrations_path` - The path where your tenant migrations are stored (only relevant for a multitenant implementation) - `:snapshots_path` - The path where the resource snapshots for the migration generator are stored. + - `:transactions_enabled?` - Due to [SQLite's single writer paradigm](https://sqlite.org/lang_transaction.html#read_transactions_versus_write_transactions) they are disabled by default. See the [transactions guide](/documentation/topics/transactions.md) for more information. """ @doc "Use this to inform the data layer about what extensions are installed" @@ -56,6 +57,7 @@ defmodule AshSqlite.Repo do |> Keyword.put(:installed_extensions, installed_extensions()) |> Keyword.put(:migrations_path, migrations_path()) |> Keyword.put(:case_sensitive_like, :on) + |> Keyword.put_new(:transactions_enabled?, false) {:ok, new_config} end diff --git a/priv/resource_snapshots/transacting_repo/transacting_posts/20250525053353.json b/priv/resource_snapshots/transacting_repo/transacting_posts/20250525053353.json new file mode 100644 index 0000000..55dbdc1 --- /dev/null +++ b/priv/resource_snapshots/transacting_repo/transacting_posts/20250525053353.json @@ -0,0 +1,47 @@ +{ + "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": "text", + "source": "subtitle", + "references": null, + "allow_nil?": true, + "generated?": false, + "primary_key?": false + } + ], + "table": "transacting_posts", + "hash": "56FE4F305257AC15BFA6AFA639A1D0A069C932004E89966DE7136AD9C96E1F93", + "repo": "Elixir.AshSqlite.TransactingRepo", + "identities": [], + "custom_indexes": [], + "base_filter": null, + "custom_statements": [], + "multitenancy": { + "global": null, + "attribute": null, + "strategy": null + }, + "has_create_action": true +} \ No newline at end of file diff --git a/priv/transacting_repo/migrations/20250525053353_add_transacting_repo.exs b/priv/transacting_repo/migrations/20250525053353_add_transacting_repo.exs new file mode 100644 index 0000000..848c0c9 --- /dev/null +++ b/priv/transacting_repo/migrations/20250525053353_add_transacting_repo.exs @@ -0,0 +1,21 @@ +defmodule AshSqlite.TransactingRepo.Migrations.AddTransactingRepo 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(:transacting_posts, primary_key: false) do + add :subtitle, :text + add :title, :text + add :id, :uuid, null: false, primary_key: true + end + end + + def down do + drop table(:transacting_posts) + end +end \ No newline at end of file diff --git a/test/support/domain.ex b/test/support/domain.ex index 90c0680..05a687b 100644 --- a/test/support/domain.ex +++ b/test/support/domain.ex @@ -1,5 +1,6 @@ defmodule AshSqlite.Test.Domain do @moduledoc false + use Ash.Domain resources do @@ -15,6 +16,7 @@ defmodule AshSqlite.Test.Domain do resource(AshSqlite.Test.Account) resource(AshSqlite.Test.Organization) resource(AshSqlite.Test.Manager) + resource(AshSqlite.Test.TransactingPost) end authorization do diff --git a/test/support/repo_case.ex b/test/support/repo_case.ex index a405788..d159829 100644 --- a/test/support/repo_case.ex +++ b/test/support/repo_case.ex @@ -17,10 +17,11 @@ defmodule AshSqlite.RepoCase do end setup tags do - :ok = Sandbox.checkout(AshSqlite.TestRepo) + repo = tags[:repo] || AshSqlite.TestRepo + :ok = Sandbox.checkout(repo) unless tags[:async] do - Sandbox.mode(AshSqlite.TestRepo, {:shared, self()}) + Sandbox.mode(repo, {:shared, self()}) end :ok diff --git a/test/support/transacting_post.ex b/test/support/transacting_post.ex new file mode 100644 index 0000000..b58f50e --- /dev/null +++ b/test/support/transacting_post.ex @@ -0,0 +1,21 @@ +defmodule AshSqlite.Test.TransactingPost do + @moduledoc false + use Ash.Resource, + domain: AshSqlite.Test.Domain, + data_layer: AshSqlite.DataLayer + + sqlite do + table("transacting_posts") + repo AshSqlite.TransactingRepo + end + + actions do + defaults([:read, :destroy, update: :*, create: :*]) + end + + attributes do + uuid_primary_key(:id, writable?: true) + attribute(:title, :string, public?: true) + attribute(:subtitle, :string, public?: true) + end +end diff --git a/test/support/transacting_repo.ex b/test/support/transacting_repo.ex new file mode 100644 index 0000000..fb51764 --- /dev/null +++ b/test/support/transacting_repo.ex @@ -0,0 +1,4 @@ +defmodule AshSqlite.TransactingRepo do + @moduledoc false + use AshSqlite.Repo, otp_app: :ash_sqlite, transactions_enabled?: true +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 5329339..fe5f14e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -2,5 +2,7 @@ ExUnit.start() ExUnit.configure(stacktrace_depth: 100) AshSqlite.TestRepo.start_link() - Ecto.Adapters.SQL.Sandbox.mode(AshSqlite.TestRepo, :manual) + +AshSqlite.TransactingRepo.start_link() +Ecto.Adapters.SQL.Sandbox.mode(AshSqlite.TransactingRepo, :manual) diff --git a/test/transaction_test.exs b/test/transaction_test.exs new file mode 100644 index 0000000..2ba1fb7 --- /dev/null +++ b/test/transaction_test.exs @@ -0,0 +1,50 @@ +defmodule AshSqlite.TransactionTest do + @moduledoc false + use AshSqlite.RepoCase, async: false + + describe "transactions are allowed when enabled" do + @describetag repo: AshSqlite.TransactingRepo + + alias AshSqlite.Test.TransactingPost, as: Post + + test "manual transaction" do + post_id = Ash.UUID.generate() + + Ash.transaction(Post, fn -> + Post + |> Ash.create!( + %{ + id: post_id, + title: "George McFly Murdered", + subtitle: "Local Author Shot Dead" + }, + transaction?: true + ) + end) + + Ash.get!(Post, post_id) + end + end + + describe "transactions are disallowed when disabled" do + @describetag repo: AshSqlite.TestRepo + alias AshSqlite.Test.Post + + test "manual transaction" do + post_id = Ash.UUID.generate() + + Ash.transaction(Post, fn -> + Post + |> Ash.create!( + %{ + id: post_id, + title: "George McFly Murdered" + }, + transaction?: true + ) + end) + + Ash.get!(Post, post_id) + end + end +end