Skip to content

Commit d2814c1

Browse files
danicodes01Daniel
andauthored
Add Options parameter to soft delete functions (#187) (#188)
* Add Options parameter to soft delete functions (#187) - Adds optional opts parameter to soft_delete/2, soft_deleteand soft_delete_all/2 - Added tests to cover the new functionality - Maintaions backward compatibility with existing code - Enables use of options like 'prefix' for multi-tenant databases - Updates function documentation with examples showing options usage - Adds section to README about using options with soft delete functions * Fix documentation references to maintain callback compatibility * Add test for log option in soft_delete_all function (#187) - Adds test to verify that log option is properly passed to Ecto - Imported ExUnit.CaptureLog in test module to capture all log output generated within the function passed to it. Returns output as a string to assert against - Confirms SQL queries are loggged when log: :info is passed - Removed unnecessary comments - Further validates that all Ecto options are correctly handled * Simplify soft delete functions (#187) - Remove unnecessary case statements and vars in soft_delete functions - Pass options directly to underlying Ecto functions - Kept same functionality while better following Elixir style conventions, and improving readability * Refactor soft delete functions with pipe operator (#188) - Replace varialbe assignment with pipe operators in soft_delete functions --------- Co-authored-by: Daniel <[email protected]>
1 parent 686fe5c commit d2814c1

File tree

3 files changed

+99
-12
lines changed

3 files changed

+99
-12
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,17 @@ post = Repo.get!(Post, 42)
117117
struct = Repo.soft_delete!(post)
118118
```
119119

120+
### Using Options with Soft Delete Functions
121+
All soft delete functions support the same options as their Ecto counterparts:
122+
123+
```elixir
124+
# With schema prefix for multi-tenant databases
125+
Repo.soft_delete(post, prefix: "tenant_abc")
126+
Repo.soft_delete!(post, prefix: "tenant_abc")
127+
Repo.soft_delete_all(Post, prefix: "tenant_abc")
128+
```
129+
This allows for seamless integration with features like PostgreSQL schema prefixes for multi-tenancy.
130+
120131
`Ecto.SoftDelete.Repo` will also intercept all queries made with the repo and automatically add a clause to filter out soft-deleted rows.
121132

122133
## Installation

lib/ecto/soft_delete_repo.ex

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,21 @@ defmodule Ecto.SoftDelete.Repo do
1818
result as second element. The second element is `nil` by default
1919
unless a `select` is supplied in the update query.
2020
21+
## Options
22+
23+
All options supported by `c:Ecto.Repo.update_all/3` can be used.
24+
2125
## Examples
2226
2327
MyRepo.soft_delete_all(Post)
2428
from(p in Post, where: p.id < 10) |> MyRepo.soft_delete_all()
2529
30+
# With schema prefix for multi-tenant databases
31+
MyRepo.soft_delete_all(Post, prefix: "tenant_abc")
32+
2633
"""
27-
@callback soft_delete_all(queryable :: Ecto.Queryable.t()) :: {integer, nil | [term]}
34+
@callback soft_delete_all(queryable :: Ecto.Queryable.t(), opts :: Keyword.t()) ::
35+
{integer, nil | [term()]}
2836

2937
@doc """
3038
Soft deletes a struct.
@@ -33,6 +41,10 @@ defmodule Ecto.SoftDelete.Repo do
3341
soft deleted or `{:error, changeset}` if there was a validation
3442
or a known constraint error.
3543
44+
## Options
45+
46+
All options supported by `c:Ecto.Repo.update/2` can be used.
47+
3648
## Examples
3749
3850
post = MyRepo.get!(Post, 42)
@@ -41,35 +53,44 @@ defmodule Ecto.SoftDelete.Repo do
4153
{:error, changeset} -> # Something went wrong
4254
end
4355
56+
# With schema prefix for multi-tenant databases
57+
MyRepo.soft_delete(post, prefix: "tenant_abc")
58+
4459
"""
45-
@callback soft_delete(struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t()) ::
46-
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
60+
@callback soft_delete(
61+
struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(),
62+
opts :: Keyword.t()
63+
) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
4764

4865
@doc """
49-
Same as `c:soft_delete/1` but returns the struct or raises if the changeset is invalid.
66+
Same as `c:soft_delete/2` but returns the struct or raises if the changeset is invalid.
5067
"""
51-
@callback soft_delete!(struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t()) ::
52-
Ecto.Schema.t()
68+
@callback soft_delete!(
69+
struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(),
70+
opts :: Keyword.t()
71+
) :: Ecto.Schema.t()
5372

5473
defmacro __using__(_opts) do
5574
quote do
5675
import Ecto.Query
5776
import Ecto.SoftDelete.Query
5877

59-
def soft_delete_all(queryable) do
60-
update_all(queryable, set: [deleted_at: DateTime.utc_now()])
78+
def soft_delete_all(queryable, opts \\ []) do
79+
set_expr = [set: [deleted_at: DateTime.utc_now()]]
80+
update_all(queryable, set_expr, opts)
6181
end
6282

63-
def soft_delete(struct_or_changeset) do
83+
# Define soft_delete and soft_delete! with options
84+
def soft_delete(struct_or_changeset, opts \\ []) do
6485
struct_or_changeset
6586
|> Ecto.Changeset.change(deleted_at: DateTime.utc_now())
66-
|> update()
87+
|> __MODULE__.update(opts)
6788
end
6889

69-
def soft_delete!(struct_or_changeset) do
90+
def soft_delete!(struct_or_changeset, opts \\ []) do
7091
struct_or_changeset
7192
|> Ecto.Changeset.change(deleted_at: DateTime.utc_now())
72-
|> update!()
93+
|> __MODULE__.update!(opts)
7394
end
7495

7596
@doc """

test/soft_delete_repo_test.exs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule Ecto.SoftDelete.Repo.Test do
22
use ExUnit.Case
33
alias Ecto.SoftDelete.Test.Repo
44
import Ecto.Query
5+
import ExUnit.CaptureLog
56

67
defmodule User do
78
use Ecto.Schema
@@ -55,6 +56,18 @@ defmodule Ecto.SoftDelete.Repo.Test do
5556
end
5657
end
5758

59+
describe "soft_delete!/2 with options" do
60+
test "should soft delete the queryable and pass options to update!" do
61+
user = Repo.insert!(%User{email: "[email protected]"})
62+
63+
# Test with empty options list
64+
assert %User{} = Repo.soft_delete!(user, [])
65+
66+
assert %DateTime{} =
67+
Repo.get_by!(User, [email: "[email protected]"], with_deleted: true).deleted_at
68+
end
69+
end
70+
5871
describe "soft_delete/1" do
5972
test "should soft delete the queryable" do
6073
user = Repo.insert!(%User{email: "[email protected]"})
@@ -75,6 +88,18 @@ defmodule Ecto.SoftDelete.Repo.Test do
7588
end
7689
end
7790

91+
describe "soft_delete/2 with options" do
92+
test "should soft delete the queryable and pass options to update" do
93+
user = Repo.insert!(%User{email: "[email protected]"})
94+
95+
# Test with empty options list
96+
assert {:ok, %User{}} = Repo.soft_delete(user, [])
97+
98+
assert %DateTime{} =
99+
Repo.get_by!(User, [email: "[email protected]"], with_deleted: true).deleted_at
100+
end
101+
end
102+
78103
describe "soft_delete_all/1" do
79104
test "soft deleted the query" do
80105
Repo.insert!(%User{email: "[email protected]"})
@@ -100,6 +125,36 @@ defmodule Ecto.SoftDelete.Repo.Test do
100125
end
101126
end
102127

128+
describe "soft_delete_all/2 with options" do
129+
test "soft deletes the query and passes options to update_all" do
130+
Repo.insert!(%User{email: "[email protected]"})
131+
Repo.insert!(%User{email: "[email protected]"})
132+
133+
# Test with empty options list
134+
assert Repo.soft_delete_all(User, []) == {2, nil}
135+
136+
assert User |> Repo.all(with_deleted: true) |> length() == 2
137+
138+
assert %DateTime{} =
139+
Repo.get_by!(User, [email: "[email protected]"], with_deleted: true).deleted_at
140+
141+
assert %DateTime{} =
142+
Repo.get_by!(User, [email: "[email protected]"], with_deleted: true).deleted_at
143+
end
144+
145+
test "soft deletes with log option" do
146+
Repo.insert!(%User{email: "[email protected]"})
147+
148+
log =
149+
capture_log(fn ->
150+
Repo.soft_delete_all(User, log: :info)
151+
end)
152+
153+
assert log =~ "UPDATE"
154+
assert log =~ "deleted_at"
155+
end
156+
end
157+
103158
describe "prepare_query/3" do
104159
test "excludes soft deleted records by default" do
105160
user = Repo.insert!(%User{email: "[email protected]"})

0 commit comments

Comments
 (0)