deleted_at 0.3.0 → 0.4.0rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitignore +77 -15
- data/.travis.yml +36 -12
- data/Gemfile +16 -1
- data/README.md +6 -1
- data/Rakefile +3 -3
- data/deleted_at.gemspec +14 -17
- data/gemfiles/activerecord-4.0.Gemfile +3 -0
- data/gemfiles/activerecord-4.1.Gemfile +3 -0
- data/gemfiles/activerecord-4.2.Gemfile +3 -0
- data/gemfiles/activerecord-5.0.Gemfile +3 -0
- data/gemfiles/activerecord-5.1.Gemfile +3 -0
- data/lib/deleted_at.rb +23 -2
- data/lib/deleted_at/active_record/base.rb +46 -105
- data/lib/deleted_at/active_record/connection_adapters/abstract/schema_definition.rb +17 -0
- data/lib/deleted_at/active_record/relation.rb +10 -57
- data/lib/deleted_at/version.rb +1 -1
- data/lib/deleted_at/views.rb +24 -20
- data/spec/deleted_at/active_record/base_spec.rb +21 -0
- data/spec/deleted_at/active_record/relation_spec.rb +166 -0
- data/spec/deleted_at/views_spec.rb +76 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/rails/app/models/animals/dog.rb +5 -0
- data/spec/support/rails/app/models/comment.rb +6 -0
- data/spec/support/rails/app/models/post.rb +7 -0
- data/spec/support/rails/app/models/user.rb +7 -0
- data/spec/support/rails/config/database.yml +4 -0
- data/spec/support/rails/config/routes.rb +3 -0
- data/spec/support/rails/db/schema.rb +27 -0
- data/spec/support/rails/log/.gitignore +1 -0
- data/spec/support/rails/public/favicon.ico +0 -0
- metadata +57 -60
- data/bin/console +0 -10
- data/bin/setup +0 -8
@@ -0,0 +1,17 @@
|
|
1
|
+
module DeletedAt
|
2
|
+
module ActiveRecord
|
3
|
+
module ConnectionAdapters #:nodoc:
|
4
|
+
module TableDefinition
|
5
|
+
|
6
|
+
def timestamps(**options)
|
7
|
+
options[:null] = false if options[:null].nil?
|
8
|
+
|
9
|
+
column(:created_at, :datetime, options)
|
10
|
+
column(:updated_at, :datetime, options)
|
11
|
+
column(:deleted_at, :datetime, options.merge(null: true)) if options[:deleted_at]
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -4,11 +4,6 @@ module DeletedAt
|
|
4
4
|
# = Active Record Relation
|
5
5
|
module Relation
|
6
6
|
|
7
|
-
def deleted_at_attributes
|
8
|
-
# We _do_ have klass at this point
|
9
|
-
{ klass.deleted_at_column => Time.now.utc }
|
10
|
-
end
|
11
|
-
|
12
7
|
# Deletes the records matching +conditions+ without instantiating the records
|
13
8
|
# first, and hence not calling the +destroy+ method nor invoking callbacks. This
|
14
9
|
# is a single SQL DELETE statement that goes straight to the database, much more
|
@@ -28,63 +23,21 @@ module DeletedAt
|
|
28
23
|
#
|
29
24
|
# Post.limit(100).delete_all
|
30
25
|
# # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
|
31
|
-
def delete_all(
|
26
|
+
def delete_all(*args)
|
27
|
+
conditions = args.pop
|
32
28
|
if archive_with_deleted_at?
|
33
|
-
|
29
|
+
if conditions
|
30
|
+
ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
|
31
|
+
Passing conditions to delete_all is not supported in DeletedAt
|
32
|
+
To achieve the same use where(conditions).delete_all.
|
33
|
+
MESSAGE
|
34
|
+
end
|
35
|
+
update_all(klass.deleted_at_attributes)
|
34
36
|
else
|
35
|
-
super
|
37
|
+
super() # Specifically drop args
|
36
38
|
end
|
37
39
|
end
|
38
40
|
|
39
|
-
# Deletes the row with a primary key matching the +id+ argument, using a
|
40
|
-
# SQL +DELETE+ statement, and returns the number of rows deleted. Active
|
41
|
-
# Record objects are not instantiated, so the object's callbacks are not
|
42
|
-
# executed, including any <tt>:dependent</tt> association options.
|
43
|
-
#
|
44
|
-
# You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
|
45
|
-
#
|
46
|
-
# Note: Although it is often much faster than the alternative,
|
47
|
-
# <tt>#destroy</tt>, skipping callbacks might bypass business logic in
|
48
|
-
# your application that ensures referential integrity or performs other
|
49
|
-
# essential jobs.
|
50
|
-
#
|
51
|
-
# ==== Examples
|
52
|
-
#
|
53
|
-
# # Delete a single row
|
54
|
-
# Todo.delete(1)
|
55
|
-
#
|
56
|
-
# # Delete multiple rows
|
57
|
-
# Todo.delete([2,3,4])
|
58
|
-
def delete(id_or_array)
|
59
|
-
where(primary_key => id_or_array).delete_all
|
60
|
-
end
|
61
|
-
|
62
|
-
# Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
|
63
|
-
# therefore all callbacks and filters are fired off before the object is deleted. This method is
|
64
|
-
# less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
|
65
|
-
#
|
66
|
-
# This essentially finds the object (or multiple objects) with the given id, creates a new object
|
67
|
-
# from the attributes, and then calls destroy on it.
|
68
|
-
#
|
69
|
-
# ==== Parameters
|
70
|
-
#
|
71
|
-
# * +id+ - Can be either an Integer or an Array of Integers.
|
72
|
-
#
|
73
|
-
# ==== Examples
|
74
|
-
#
|
75
|
-
# # Destroy a single object
|
76
|
-
# Todo.destroy(1)
|
77
|
-
#
|
78
|
-
# # Destroy multiple objects
|
79
|
-
# todos = [1,2,3]
|
80
|
-
# Todo.destroy(todos)
|
81
|
-
def destroy(id)
|
82
|
-
if id.is_a?(Array)
|
83
|
-
id.map { |one_id| destroy(one_id) }
|
84
|
-
else
|
85
|
-
find(id).destroy
|
86
|
-
end
|
87
|
-
end
|
88
41
|
end
|
89
42
|
end
|
90
43
|
end
|
data/lib/deleted_at/version.rb
CHANGED
data/lib/deleted_at/views.rb
CHANGED
@@ -3,23 +3,27 @@ module DeletedAt
|
|
3
3
|
|
4
4
|
def self.install_present_view(model)
|
5
5
|
uninstall_present_view(model)
|
6
|
-
all_table_name = all_table(model)
|
7
6
|
present_table_name = present_view(model)
|
8
7
|
|
9
|
-
model
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
while_spoofing_table_name(model, all_table(model)) do
|
9
|
+
model.connection.execute("ALTER TABLE \"#{present_table_name}\" RENAME TO \"#{model.table_name}\"")
|
10
|
+
model.connection.execute <<-SQL
|
11
|
+
CREATE OR REPLACE VIEW "#{present_table_name}"
|
12
|
+
AS #{ model.where(model.deleted_at_column => nil).to_sql }
|
13
|
+
SQL
|
14
|
+
end
|
14
15
|
end
|
15
16
|
|
16
17
|
def self.install_deleted_view(model)
|
17
|
-
return warn("You must install the all/present tables/views first!") unless all_table_exists?(model)
|
18
|
+
return DeletedAt.logger.warn("You must install the all/present tables/views first!") unless all_table_exists?(model)
|
18
19
|
table_name = deleted_view(model)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
|
21
|
+
while_spoofing_table_name(model, all_table(model)) do
|
22
|
+
model.connection.execute <<-SQL
|
23
|
+
CREATE OR REPLACE VIEW "#{table_name}"
|
24
|
+
AS #{ model.where.not(model.deleted_at_column => nil).to_sql }
|
25
|
+
SQL
|
26
|
+
end
|
23
27
|
end
|
24
28
|
|
25
29
|
def self.all_table_exists?(model)
|
@@ -28,9 +32,9 @@ module DeletedAt
|
|
28
32
|
SELECT 1
|
29
33
|
FROM information_schema.tables
|
30
34
|
WHERE table_name = '#{all_table(model)}'
|
31
|
-
);
|
35
|
+
) AS exists;
|
32
36
|
SQL
|
33
|
-
|
37
|
+
DeletedAt.testify(query.first['exists'])
|
34
38
|
end
|
35
39
|
|
36
40
|
def self.deleted_view_exists?(model)
|
@@ -39,9 +43,9 @@ module DeletedAt
|
|
39
43
|
SELECT 1
|
40
44
|
FROM information_schema.tables
|
41
45
|
WHERE table_name = '#{deleted_view(model)}'
|
42
|
-
);
|
46
|
+
) AS exists;
|
43
47
|
SQL
|
44
|
-
|
48
|
+
DeletedAt.testify(query.first['exists'])
|
45
49
|
end
|
46
50
|
|
47
51
|
def self.present_view(model)
|
@@ -71,11 +75,11 @@ module DeletedAt
|
|
71
75
|
|
72
76
|
private
|
73
77
|
|
74
|
-
def self.
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
78
|
+
def self.while_spoofing_table_name(model, new_name, &block)
|
79
|
+
old_name = model.table_name
|
80
|
+
model.table_name = new_name
|
81
|
+
yield
|
82
|
+
model.table_name = old_name
|
79
83
|
end
|
80
84
|
|
81
85
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe DeletedAt::ActiveRecord::Base do
|
4
|
+
|
5
|
+
context "model missing deleted_at column" do
|
6
|
+
|
7
|
+
it "fails when trying to install" do
|
8
|
+
expect(DeletedAt.install(Comment)).to eq(false)
|
9
|
+
expect(DeletedAt.uninstall(Comment)).to eq(false)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "warns when using with_deleted_at" do
|
13
|
+
expected_stderr = "Missing `deleted_at` in `Comment` when trying to employ `deleted_at`"
|
14
|
+
allow(Comment).to receive(:has_deleted_at_views?).and_return(true)
|
15
|
+
expect(DeletedAt.logger).to receive(:warn).with(expected_stderr)
|
16
|
+
Comment.with_deleted_at
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe DeletedAt::ActiveRecord::Relation do
|
4
|
+
|
5
|
+
context "models using deleted_at" do
|
6
|
+
|
7
|
+
it "#destroy should set deleted_at" do
|
8
|
+
DeletedAt.install(User)
|
9
|
+
User.create(name: 'bob')
|
10
|
+
User.create(name: 'john')
|
11
|
+
User.create(name: 'sally')
|
12
|
+
|
13
|
+
User.first.destroy
|
14
|
+
|
15
|
+
expect(User.count).to eq(2)
|
16
|
+
expect(User::All.count).to eq(3)
|
17
|
+
expect(User::Deleted.count).to eq(1)
|
18
|
+
DeletedAt.uninstall(User)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "#delete should set deleted_at" do
|
22
|
+
DeletedAt.install(User)
|
23
|
+
User.create(name: 'bob')
|
24
|
+
User.create(name: 'john')
|
25
|
+
User.create(name: 'sally')
|
26
|
+
|
27
|
+
User.first.delete
|
28
|
+
|
29
|
+
expect(User.count).to eq(2)
|
30
|
+
expect(User::All.count).to eq(3)
|
31
|
+
expect(User::Deleted.count).to eq(1)
|
32
|
+
DeletedAt.uninstall(User)
|
33
|
+
end
|
34
|
+
|
35
|
+
context '#destroy_all' do
|
36
|
+
it "should set deleted_at" do
|
37
|
+
DeletedAt.install(User)
|
38
|
+
User.create(name: 'bob')
|
39
|
+
User.create(name: 'john')
|
40
|
+
User.create(name: 'sally')
|
41
|
+
|
42
|
+
User.all.destroy_all
|
43
|
+
|
44
|
+
expect(User.count).to eq(0)
|
45
|
+
expect(User::All.count).to eq(3)
|
46
|
+
expect(User::Deleted.count).to eq(3)
|
47
|
+
DeletedAt.uninstall(User)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "with conditions should set deleted_at" do
|
51
|
+
DeletedAt.install(User)
|
52
|
+
User.create(name: 'bob')
|
53
|
+
User.create(name: 'john')
|
54
|
+
User.create(name: 'sally')
|
55
|
+
|
56
|
+
User.where(name: 'bob').destroy_all
|
57
|
+
|
58
|
+
expect(User.count).to eq(2)
|
59
|
+
expect(User::All.count).to eq(3)
|
60
|
+
expect(User::Deleted.count).to eq(1)
|
61
|
+
DeletedAt.uninstall(User)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context '#delete_all' do
|
66
|
+
it "should set deleted_at" do
|
67
|
+
DeletedAt.install(Animals::Dog)
|
68
|
+
Animals::Dog.create(name: 'bob')
|
69
|
+
Animals::Dog.create(name: 'john')
|
70
|
+
Animals::Dog.create(name: 'sally')
|
71
|
+
|
72
|
+
# conditions should not matter
|
73
|
+
Animals::Dog.all.delete_all(name: 'bob')
|
74
|
+
|
75
|
+
expect(Animals::Dog.count).to eq(0)
|
76
|
+
expect(Animals::Dog::All.count).to eq(3)
|
77
|
+
expect(Animals::Dog::Deleted.count).to eq(3)
|
78
|
+
DeletedAt.uninstall(Animals::Dog)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "with conditions should set deleted_at" do
|
82
|
+
DeletedAt.install(Animals::Dog)
|
83
|
+
Animals::Dog.create(name: 'bob')
|
84
|
+
Animals::Dog.create(name: 'john')
|
85
|
+
Animals::Dog.create(name: 'sally')
|
86
|
+
|
87
|
+
Animals::Dog.where(name: 'bob').delete_all
|
88
|
+
|
89
|
+
expect(Animals::Dog.count).to eq(2)
|
90
|
+
expect(Animals::Dog::All.count).to eq(3)
|
91
|
+
expect(Animals::Dog::Deleted.count).to eq(1)
|
92
|
+
DeletedAt.uninstall(Animals::Dog)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
context "models not using deleted_at" do
|
99
|
+
|
100
|
+
it "#destroy should actually delete the record" do
|
101
|
+
Comment.create(title: 'Agree')
|
102
|
+
Comment.create(title: 'Disagree')
|
103
|
+
Comment.create(title: 'Defer')
|
104
|
+
|
105
|
+
Comment.first.destroy
|
106
|
+
|
107
|
+
expect(Comment.count).to eq(2)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "#delete should actually delete the record" do
|
111
|
+
Comment.create(title: 'Agree')
|
112
|
+
Comment.create(title: 'Disagree')
|
113
|
+
Comment.create(title: 'Defer')
|
114
|
+
|
115
|
+
Comment.first.delete
|
116
|
+
|
117
|
+
expect(Comment.count).to eq(2)
|
118
|
+
end
|
119
|
+
|
120
|
+
context '#destroy_all' do
|
121
|
+
it "should actually delete records" do
|
122
|
+
Comment.create(title: 'Agree')
|
123
|
+
Comment.create(title: 'Disagree')
|
124
|
+
Comment.create(title: 'Defer')
|
125
|
+
|
126
|
+
Comment.all.destroy_all
|
127
|
+
|
128
|
+
expect(Comment.count).to eq(0)
|
129
|
+
end
|
130
|
+
|
131
|
+
it "with conditions should actually delete records" do
|
132
|
+
Comment.create(title: 'Agree')
|
133
|
+
Comment.create(title: 'Disagree')
|
134
|
+
Comment.create(title: 'Defer')
|
135
|
+
|
136
|
+
Comment.where(title: 'Disagree').destroy_all
|
137
|
+
|
138
|
+
expect(Comment.count).to eq(2)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context '#delete_all' do
|
143
|
+
it "should actually delete records" do
|
144
|
+
Comment.create(title: 'Agree')
|
145
|
+
Comment.create(title: 'Disagree')
|
146
|
+
Comment.create(title: 'Defer')
|
147
|
+
|
148
|
+
Comment.all.delete_all
|
149
|
+
|
150
|
+
expect(Comment.count).to eq(0)
|
151
|
+
end
|
152
|
+
|
153
|
+
it "with conditions should actually delete records" do
|
154
|
+
Comment.create(title: 'Agree')
|
155
|
+
Comment.create(title: 'Disagree')
|
156
|
+
Comment.create(title: 'Defer')
|
157
|
+
|
158
|
+
Comment.where(title: 'Agree').delete_all
|
159
|
+
|
160
|
+
expect(Comment.count).to eq(2)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe DeletedAt::Views do
|
4
|
+
{
|
5
|
+
User => "simple model",
|
6
|
+
Post => "model with customized table_name",
|
7
|
+
Animals::Dog => "namespaced model"
|
8
|
+
}.each do |model, description|
|
9
|
+
|
10
|
+
plural = model.name.pluralize
|
11
|
+
table_name = model.table_name
|
12
|
+
|
13
|
+
describe "#install for #{description}" do
|
14
|
+
after(:each) do
|
15
|
+
DeletedAt.uninstall(model)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should not raise error' do
|
19
|
+
expect{ DeletedAt.install(model) }.to_not raise_error()
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should rename the models table' do
|
23
|
+
DeletedAt.install(model)
|
24
|
+
expect(ActiveRecord::Base.connection.table_exists?("#{table_name}/all")).to be_truthy
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should have a view for all non-deleted #{plural}" do
|
28
|
+
DeletedAt.install(model)
|
29
|
+
if Gem::Version.new(Rails.version) < Gem::Version.new("5.0")
|
30
|
+
expect(ActiveRecord::Base.connection.table_exists?(table_name)).to be_truthy
|
31
|
+
else
|
32
|
+
expect(ActiveRecord::Base.connection.view_exists?(table_name)).to be_truthy
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should have a view for all deleted #{plural}" do
|
37
|
+
DeletedAt.install(model)
|
38
|
+
if Gem::Version.new(Rails.version) < Gem::Version.new("5.0")
|
39
|
+
expect(ActiveRecord::Base.connection.table_exists?("#{table_name}/deleted")).to be_truthy
|
40
|
+
else
|
41
|
+
expect(ActiveRecord::Base.connection.view_exists?("#{table_name}/deleted")).to be_truthy
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'creates the DeletedAt class extensions' do
|
46
|
+
DeletedAt.install(model)
|
47
|
+
expect(model.const_defined?(:All)).to be_truthy
|
48
|
+
expect(model.const_defined?(:Deleted)).to be_truthy
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'sets the correct table name for modified class' do
|
52
|
+
DeletedAt.install(model)
|
53
|
+
expect(model.table_name).to eql(table_name)
|
54
|
+
expect(model::All.table_name).to eql("#{table_name}/all")
|
55
|
+
expect(model::Deleted.table_name).to eql("#{table_name}/deleted")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#uninstall for #{description}" do
|
60
|
+
before(:each) do
|
61
|
+
DeletedAt.install(model)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should not raise error' do
|
65
|
+
expect{ DeletedAt.uninstall(model) }.to_not raise_error()
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should remove model extensions' do
|
69
|
+
DeletedAt.uninstall(model)
|
70
|
+
expect(model.const_defined?(:All)).to be_falsy
|
71
|
+
expect(model.const_defined?(:Deleted)).to be_falsy
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|