Skip to content

Commit 40971dc

Browse files
committed
improvement: Add flag to enable transactions for write actions.
1 parent a7cfd17 commit 40971dc

File tree

10 files changed

+135
-12
lines changed

10 files changed

+135
-12
lines changed

.formatter.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ spark_locals_without_parens = [
33
code?: 1,
44
deferrable: 1,
55
down: 1,
6+
enable_write_transactions?: 1,
67
exclusion_constraint_names: 1,
78
foreign_key_names: 1,
89
identity_index_names: 1,

documentation/dsls/DSL:-AshSqlite.DataLayer.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ end
4848
| [`migration_ignore_attributes`](#sqlite-migration_ignore_attributes){: #sqlite-migration_ignore_attributes } | `list(atom)` | `[]` | A list of attributes that will be ignored when generating migrations. |
4949
| [`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. |
5050
| [`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. |
51+
| [`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. |
5152

5253

5354
## sqlite.custom_indexes

lib/data_layer.ex

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ defmodule AshSqlite.DataLayer do
194194
],
195195
schema: [
196196
repo: [
197-
type: :atom,
197+
type: {:or, [{:behaviour, Ecto.Repo}, {:fun, 2}]},
198198
required: true,
199199
doc:
200200
"The repo that will be used to fetch your data. See the `AshSqlite.Repo` documentation for more"
@@ -310,6 +310,30 @@ defmodule AshSqlite.DataLayer do
310310
Mix.Task.run("ash_sqlite.migrate", args)
311311
end
312312

313+
@impl true
314+
def transaction(resource, func, timeout \\ nil, reason \\ %{type: :custom, metadata: %{}}) do
315+
repo =
316+
case reason[:data_layer_context] do
317+
%{repo: repo} when not is_nil(repo) ->
318+
repo
319+
320+
_ ->
321+
AshSqlite.DataLayer.Info.repo(resource, :read)
322+
end
323+
324+
func = fn ->
325+
repo.on_transaction_begin(reason)
326+
327+
func.()
328+
end
329+
330+
if timeout do
331+
repo.transaction(func, timeout: timeout)
332+
else
333+
repo.transaction(func)
334+
end
335+
end
336+
313337
def rollback(args) do
314338
repos = AshSqlite.Mix.Helpers.repos!([], args)
315339

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

395-
def can?(_, :transact), do: false
419+
def can?(resource, :transact) do
420+
Spark.Dsl.Extension.get_opt(resource, [:sqlite], :enable_write_transactions?, false)
421+
end
422+
396423
def can?(_, :composite_primary_key), do: true
397424
def can?(_, {:atomic, :update}), do: true
398425
def can?(_, {:atomic, :upsert}), do: true
@@ -453,6 +480,11 @@ defmodule AshSqlite.DataLayer do
453480
def can?(_, {:sort, _}), do: true
454481
def can?(_, _), do: false
455482

483+
@impl true
484+
def in_transaction?(resource) do
485+
AshSqlite.DataLayer.Info.repo(resource, :mutate).in_transaction?()
486+
end
487+
456488
@impl true
457489
def limit(query, nil, _), do: {:ok, query}
458490

lib/data_layer/info.ex

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ defmodule AshSqlite.DataLayer.Info do
44
alias Spark.Dsl.Extension
55

66
@doc "The configured repo for a resource"
7-
def repo(resource) do
8-
Extension.get_opt(resource, [:sqlite], :repo, nil, true)
7+
def repo(resource, type \\ :mutate) do
8+
case Extension.get_opt(resource, [:sqlite], :repo, nil, true) do
9+
fun when is_function(fun, 2) ->
10+
fun.(resource, type)
11+
12+
repo ->
13+
repo
14+
end
915
end
1016

1117
@doc "The configured table for a resource"

lib/repo.ex

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,9 @@ defmodule AshSqlite.Repo do
1212
@doc "Use this to inform the data layer about what extensions are installed"
1313
@callback installed_extensions() :: [String.t()]
1414

15-
@doc """
16-
Use this to inform the data layer about the oldest potential sqlite version it will be run on.
1715

18-
Must be an integer greater than or equal to 13.
19-
"""
20-
@callback min_pg_version() :: integer()
16+
@doc "Called when a transaction starts"
17+
@callback on_transaction_begin(reason :: Ash.DataLayer.transaction_reason()) :: term
2118

2219
@doc "The path where your migrations are stored"
2320
@callback migrations_path() :: String.t() | nil
@@ -39,7 +36,6 @@ defmodule AshSqlite.Repo do
3936
def installed_extensions, do: []
4037
def migrations_path, do: nil
4138
def override_migration_type(type), do: type
42-
def min_pg_version, do: 10
4339

4440
def init(_, config) do
4541
new_config =
@@ -51,6 +47,8 @@ defmodule AshSqlite.Repo do
5147
{:ok, new_config}
5248
end
5349

50+
def on_transaction_begin(_reason), do: :ok
51+
5452
def insert(struct_or_changeset, opts \\ []) do
5553
struct_or_changeset
5654
|> to_ecto()
@@ -82,6 +80,13 @@ defmodule AshSqlite.Repo do
8280
end)
8381
|> from_ecto()
8482
end
83+
84+
def transaction!(fun) do
85+
case fun.() do
86+
{:ok, value} -> value
87+
{:error, error} -> raise Ash.Error.to_error_class(error)
88+
end
89+
end
8590

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

149154
defoverridable init: 2,
150155
installed_extensions: 0,
151-
override_migration_type: 1,
152-
min_pg_version: 0
156+
on_transaction_begin: 1,
157+
override_migration_type: 1
153158
end
154159
end
155160
end

test/support/domain.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ defmodule AshSqlite.Test.Domain do
1515
resource(AshSqlite.Test.Account)
1616
resource(AshSqlite.Test.Organization)
1717
resource(AshSqlite.Test.Manager)
18+
allow_unregistered?(true)
1819
end
1920

2021
authorization do

test/support/resources/comment.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ defmodule AshSqlite.Test.Comment do
3232
argument(:rating, :map)
3333

3434
change(manage_relationship(:rating, :ratings, on_missing: :ignore, on_match: :create))
35+
transaction?(false)
3536
end
3637
end
3738

test/support/resources/manager.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ defmodule AshSqlite.Test.Manager do
1818
argument(:organization_id, :uuid, allow_nil?: false)
1919

2020
change(manage_relationship(:organization_id, :organization, type: :append_and_remove))
21+
transaction?(false)
2122
end
2223
end
2324

test/support/resources/post.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@ defmodule AshSqlite.Test.Post do
5454
on_match: :create
5555
)
5656
)
57+
58+
transaction?(false)
5759
end
5860

5961
update :increment_score do
6062
argument(:amount, :integer, default: 1)
6163
change(atomic_update(:score, expr((score || 0) + ^arg(:amount))))
64+
transaction?(false)
6265
end
6366
end
6467

test/transaction_test.exs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
defmodule AshSqlite.TransactionTest do
2+
@moduledoc false
3+
use AshSqlite.RepoCase, async: false
4+
5+
test "transactions cannot be used by default" do
6+
assert_raise(Spark.Error.DslError, ~r/transaction\? false/, fn ->
7+
defmodule ShouldNotCompileResource do
8+
use Ash.Resource,
9+
domain: AshSqlite.Test.Domain,
10+
data_layer: AshSqlite.DataLayer,
11+
validate_domain_inclusion?: false
12+
13+
sqlite do
14+
repo AshSqlite.TestRepo
15+
table "should_not_compile_resource"
16+
end
17+
18+
attributes do
19+
uuid_primary_key(:id)
20+
end
21+
22+
actions do
23+
create :create do
24+
primary?(true)
25+
transaction?(true)
26+
end
27+
end
28+
end
29+
end)
30+
end
31+
32+
test "transactions can be enabled however" do
33+
defmodule TransactionalResource do
34+
use Ash.Resource,
35+
domain: AshSqlite.Test.Domain,
36+
data_layer: AshSqlite.DataLayer,
37+
validate_domain_inclusion?: false
38+
39+
sqlite do
40+
repo AshSqlite.TestRepo
41+
table "accounts"
42+
enable_write_transactions? true
43+
end
44+
45+
attributes do
46+
uuid_primary_key(:id)
47+
attribute(:is_active, :boolean, public?: true)
48+
end
49+
50+
actions do
51+
create :create do
52+
accept []
53+
primary?(true)
54+
transaction?(true)
55+
change fn changeset, _context ->
56+
transaction? = AshSqlite.TestRepo.in_transaction?() |> dbg()
57+
58+
changeset
59+
|> Ash.Changeset.change_attribute(:is_active, transaction?)
60+
end
61+
end
62+
end
63+
end
64+
65+
record =
66+
TransactionalResource
67+
|> Ash.Changeset.for_create(:create, %{})
68+
|> Ash.create!()
69+
70+
assert record.is_active
71+
end
72+
end

0 commit comments

Comments
 (0)