Skip to content

Commit

Permalink
improvement: Add flag to enable transactions for write actions.
Browse files Browse the repository at this point in the history
  • Loading branch information
jimsynz committed Oct 14, 2024
1 parent a7cfd17 commit 40971dc
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 12 deletions.
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ spark_locals_without_parens = [
code?: 1,
deferrable: 1,
down: 1,
enable_write_transactions?: 1,
exclusion_constraint_names: 1,
foreign_key_names: 1,
identity_index_names: 1,
Expand Down
1 change: 1 addition & 0 deletions documentation/dsls/DSL:-AshSqlite.DataLayer.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ end
| [`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. |
| [`enable_write_transactions?`](#sqlite-enable_write_transactions?){: #sqlite-enable_write_transactions? } | `boolean` | `false` | Enable write transactions for this resource. See the [SQLite transaction guide](/documentation/topics/about-as-sqlite/transactions.md) for more. |


## sqlite.custom_indexes
Expand Down
36 changes: 34 additions & 2 deletions lib/data_layer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ defmodule AshSqlite.DataLayer do
],
schema: [
repo: [
type: :atom,
type: {:or, [{:behaviour, Ecto.Repo}, {:fun, 2}]},
required: true,
doc:
"The repo that will be used to fetch your data. See the `AshSqlite.Repo` documentation for more"
Expand Down Expand Up @@ -310,6 +310,30 @@ defmodule AshSqlite.DataLayer do
Mix.Task.run("ash_sqlite.migrate", args)
end

@impl true
def transaction(resource, func, timeout \\ nil, reason \\ %{type: :custom, metadata: %{}}) do
repo =
case reason[:data_layer_context] do
%{repo: repo} when not is_nil(repo) ->
repo

_ ->
AshSqlite.DataLayer.Info.repo(resource, :read)
end

func = fn ->
repo.on_transaction_begin(reason)

func.()
end

if timeout do
repo.transaction(func, timeout: timeout)
else
repo.transaction(func)
end
end

def rollback(args) do
repos = AshSqlite.Mix.Helpers.repos!([], args)

Expand Down Expand Up @@ -392,7 +416,10 @@ defmodule AshSqlite.DataLayer do
def can?(_, :bulk_create), do: true
def can?(_, {:lock, _}), do: false

def can?(_, :transact), do: false
def can?(resource, :transact) do
Spark.Dsl.Extension.get_opt(resource, [:sqlite], :enable_write_transactions?, false)
end

def can?(_, :composite_primary_key), do: true
def can?(_, {:atomic, :update}), do: true
def can?(_, {:atomic, :upsert}), do: true
Expand Down Expand Up @@ -453,6 +480,11 @@ defmodule AshSqlite.DataLayer do
def can?(_, {:sort, _}), do: true
def can?(_, _), do: false

@impl true
def in_transaction?(resource) do
AshSqlite.DataLayer.Info.repo(resource, :mutate).in_transaction?()
end

@impl true
def limit(query, nil, _), do: {:ok, query}

Expand Down
10 changes: 8 additions & 2 deletions lib/data_layer/info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ defmodule AshSqlite.DataLayer.Info do
alias Spark.Dsl.Extension

@doc "The configured repo for a resource"
def repo(resource) do
Extension.get_opt(resource, [:sqlite], :repo, nil, true)
def repo(resource, type \\ :mutate) do
case Extension.get_opt(resource, [:sqlite], :repo, nil, true) do
fun when is_function(fun, 2) ->
fun.(resource, type)

repo ->
repo
end
end

@doc "The configured table for a resource"
Expand Down
21 changes: 13 additions & 8 deletions lib/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,9 @@ defmodule AshSqlite.Repo do
@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 "Called when a transaction starts"
@callback on_transaction_begin(reason :: Ash.DataLayer.transaction_reason()) :: term

@doc "The path where your migrations are stored"
@callback migrations_path() :: String.t() | nil
Expand All @@ -39,7 +36,6 @@ defmodule AshSqlite.Repo do
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 =
Expand All @@ -51,6 +47,8 @@ defmodule AshSqlite.Repo do
{:ok, new_config}
end

def on_transaction_begin(_reason), do: :ok

def insert(struct_or_changeset, opts \\ []) do
struct_or_changeset
|> to_ecto()
Expand Down Expand Up @@ -82,6 +80,13 @@ defmodule AshSqlite.Repo do
end)
|> from_ecto()
end

def transaction!(fun) do
case fun.() do
{:ok, value} -> value
{:error, error} -> raise Ash.Error.to_error_class(error)
end
end

def from_ecto({:ok, result}), do: {:ok, from_ecto(result)}
def from_ecto({:error, _} = other), do: other
Expand Down Expand Up @@ -148,8 +153,8 @@ defmodule AshSqlite.Repo do

defoverridable init: 2,
installed_extensions: 0,
override_migration_type: 1,
min_pg_version: 0
on_transaction_begin: 1,
override_migration_type: 1
end
end
end
1 change: 1 addition & 0 deletions test/support/domain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule AshSqlite.Test.Domain do
resource(AshSqlite.Test.Account)
resource(AshSqlite.Test.Organization)
resource(AshSqlite.Test.Manager)
allow_unregistered?(true)
end

authorization do
Expand Down
1 change: 1 addition & 0 deletions test/support/resources/comment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ defmodule AshSqlite.Test.Comment do
argument(:rating, :map)

change(manage_relationship(:rating, :ratings, on_missing: :ignore, on_match: :create))
transaction?(false)
end
end

Expand Down
1 change: 1 addition & 0 deletions test/support/resources/manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule AshSqlite.Test.Manager do
argument(:organization_id, :uuid, allow_nil?: false)

change(manage_relationship(:organization_id, :organization, type: :append_and_remove))
transaction?(false)
end
end

Expand Down
3 changes: 3 additions & 0 deletions test/support/resources/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,14 @@ defmodule AshSqlite.Test.Post do
on_match: :create
)
)

transaction?(false)
end

update :increment_score do
argument(:amount, :integer, default: 1)
change(atomic_update(:score, expr((score || 0) + ^arg(:amount))))
transaction?(false)
end
end

Expand Down
72 changes: 72 additions & 0 deletions test/transaction_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
defmodule AshSqlite.TransactionTest do
@moduledoc false
use AshSqlite.RepoCase, async: false

test "transactions cannot be used by default" do

Check failure on line 5 in test/transaction_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci / mix test

test transactions cannot be used by default (AshSqlite.TransactionTest)
assert_raise(Spark.Error.DslError, ~r/transaction\? false/, fn ->
defmodule ShouldNotCompileResource do
use Ash.Resource,
domain: AshSqlite.Test.Domain,
data_layer: AshSqlite.DataLayer,
validate_domain_inclusion?: false

sqlite do
repo AshSqlite.TestRepo
table "should_not_compile_resource"
end

attributes do
uuid_primary_key(:id)
end

actions do
create :create do
primary?(true)
transaction?(true)
end
end
end
end)
end

test "transactions can be enabled however" do

Check failure on line 32 in test/transaction_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci / mix test

test transactions can be enabled however (AshSqlite.TransactionTest)
defmodule TransactionalResource do
use Ash.Resource,
domain: AshSqlite.Test.Domain,
data_layer: AshSqlite.DataLayer,
validate_domain_inclusion?: false

sqlite do
repo AshSqlite.TestRepo
table "accounts"
enable_write_transactions? true
end

attributes do
uuid_primary_key(:id)
attribute(:is_active, :boolean, public?: true)
end

actions do
create :create do
accept []
primary?(true)
transaction?(true)
change fn changeset, _context ->
transaction? = AshSqlite.TestRepo.in_transaction?() |> dbg()

changeset
|> Ash.Changeset.change_attribute(:is_active, transaction?)
end
end
end
end

record =
TransactionalResource
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()

assert record.is_active
end
end

0 comments on commit 40971dc

Please sign in to comment.