deleted_at 0.3.0 → 0.4.0rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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(conditions = nil)
26
+ def delete_all(*args)
27
+ conditions = args.pop
32
28
  if archive_with_deleted_at?
33
- where(conditions).update_all(deleted_at_attributes)
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
@@ -1,3 +1,3 @@
1
1
  module DeletedAt
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0rc1"
3
3
  end
@@ -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.connection.execute("ALTER TABLE \"#{present_table_name}\" RENAME TO \"#{all_table_name}\"")
10
- model.connection.execute <<-SQL
11
- CREATE OR REPLACE VIEW "#{present_table_name}"
12
- AS SELECT * FROM "#{all_table_name}" WHERE #{model.deleted_at_column} IS NULL;
13
- SQL
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
- model.connection.execute <<-SQL
20
- CREATE OR REPLACE VIEW "#{table_name}"
21
- AS SELECT * FROM "#{all_table(model)}" WHERE #{model.deleted_at_column} IS NOT NULL;
22
- SQL
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
- get_truthy_value_from_psql(query)
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
- get_truthy_value_from_psql(query)
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.get_truthy_value_from_psql(result)
75
- # Some versions of PSQL return {"?column?"=>"t"}
76
- # instead of {"first"=>"t"}, so we're saying screw it,
77
- # just give me the first value of whatever is returned
78
- result.try(:first).try(:values).try(:first) == 't'
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