activerecord-batch_touching 1.0.pre.beta → 1.0.pre.beta2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 829e262f5bb2081c84c1d8baea4afc937806119fbc605a0f13b5bccaecbf9616
4
- data.tar.gz: ed048b0b157de0f9e6dbf6dcdfe08253c95177759e692fe718eda84f7460d0d5
3
+ metadata.gz: fd952639925581e1a9402a4e1542ca8be34e826ee23754742381daea8465f59d
4
+ data.tar.gz: 061f01b40c568b24b9c383558c63f87bf29e27ded41192ae30d5afc0106294af
5
5
  SHA512:
6
- metadata.gz: 625f5ed7a40621a366cd5c110a23d7f1a4048b92d7568feec17206c6f72d3507a6d4f4a1862f472147e980f0b632420ff0afe70372c297716718ec00b6c6671f
7
- data.tar.gz: 52ba63a3c2df1cb8431ab5f17703cf51021250bba311bdcd483657ae24f4ebef5a86299c0a9e84b8869a08a08821b6188e417cdb08f62f09d9d8ec482208d725
6
+ metadata.gz: 25f107a0f99f1e951a5ec5c83115aab53c90676ffbc9652e170566fbd933fa61a18e1649a0a885a29e935fd08aa27dcc26450632c86c69230792cc48f5e6c799
7
+ data.tar.gz: a231c2bfc1aefd7d56b537c41d5a0f887660a4b4d842e1f066a6e65057402471fe8be1fc456eecb92c901f4e8b45bd13308a6c2a84c7c1f1b8343a7e14463108
@@ -2,6 +2,36 @@ require "activerecord/batch_touching/version"
2
2
  require "activerecord/batch_touching/state"
3
3
 
4
4
  module ActiveRecord
5
+ module BatchTouchingAbstractAdapter
6
+ # Batches up +touch+ calls for the duration of a transaction.
7
+ # +after_touch+ callbacks are also delayed until the transaction is committed.
8
+ #
9
+ # ==== Examples
10
+ #
11
+ # # Touches Person.first and Person.last in a single database round-trip.
12
+ # Person.transaction do
13
+ # Person.first.touch
14
+ # Person.last.touch
15
+ # end
16
+ #
17
+ # # Touches Person.first once, not twice, right before the transaction is committed.
18
+ # Person.transaction do
19
+ # Person.first.touch
20
+ # Person.first.touch
21
+ # end
22
+ def transaction(requires_new: nil, isolation: nil, joinable: true, &block)
23
+ super(requires_new: requires_new, isolation: isolation, joinable: joinable) do
24
+ BatchTouching.start(requires_new: requires_new, &block)
25
+ end
26
+ end
27
+ end
28
+
29
+ module ConnectionAdapters
30
+ class AbstractAdapter
31
+ prepend BatchTouchingAbstractAdapter
32
+ end
33
+ end
34
+
5
35
  # = Active Record Batch Touching
6
36
  module BatchTouching # :nodoc:
7
37
  extend ActiveSupport::Concern
@@ -9,7 +39,7 @@ module ActiveRecord
9
39
  # Override ActiveRecord::Base#touch_later. This will effectively disable the current built-in mechanism AR uses
10
40
  # to delay touching in favor of our method of batch touching.
11
41
  def touch_later(*names)
12
- touch(*names)
42
+ BatchTouching.batch_touching? ? touch(*names) : super
13
43
  end
14
44
 
15
45
  # Override ActiveRecord::Base#touch. If currently batching touches, always return
@@ -24,32 +54,6 @@ module ActiveRecord
24
54
  end
25
55
  end
26
56
 
27
- # These get added as class methods to ActiveRecord::Base.
28
- module ClassMethods
29
- # Batches up +touch+ calls for the duration of a transaction.
30
- # +after_touch+ callbacks are also delayed until the transaction is committed.
31
- #
32
- # ==== Examples
33
- #
34
- # # Touches Person.first and Person.last in a single database round-trip.
35
- # Person.transaction do
36
- # Person.first.touch
37
- # Person.last.touch
38
- # end
39
- #
40
- # # Touches Person.first once, not twice, right before the transaction is committed.
41
- # Person.transaction do
42
- # Person.first.touch
43
- # Person.first.touch
44
- # end
45
- #
46
- def transaction(**options, &block)
47
- super(**options) do
48
- BatchTouching.start(**options, &block)
49
- end
50
- end
51
- end
52
-
53
57
  class << self
54
58
  def states
55
59
  Thread.current[:batch_touching_states] ||= []
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-batch_touching
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.pre.beta
4
+ version: 1.0.pre.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Morearty
@@ -132,19 +132,10 @@ executables: []
132
132
  extensions: []
133
133
  extra_rdoc_files: []
134
134
  files:
135
- - ".gitignore"
136
- - ".rspec"
137
- - Gemfile
138
135
  - LICENSE
139
- - activerecord-batch_touching.gemspec
140
136
  - lib/activerecord/batch_touching.rb
141
137
  - lib/activerecord/batch_touching/state.rb
142
138
  - lib/activerecord/batch_touching/version.rb
143
- - spec/active_record/batch_touching_spec.rb
144
- - spec/rcov_exclude_list.rb
145
- - spec/spec_helper.rb
146
- - spec/support/models.rb
147
- - spec/support/schema.rb
148
139
  homepage: ''
149
140
  licenses:
150
141
  - MIT
@@ -168,9 +159,4 @@ rubygems_version: 3.3.7
168
159
  signing_key:
169
160
  specification_version: 4
170
161
  summary: Batch up your ActiveRecord "touch" operations for better performance.
171
- test_files:
172
- - spec/active_record/batch_touching_spec.rb
173
- - spec/rcov_exclude_list.rb
174
- - spec/spec_helper.rb
175
- - spec/support/models.rb
176
- - spec/support/schema.rb
162
+ test_files: []
data/.gitignore DELETED
@@ -1,4 +0,0 @@
1
- .idea
2
- .ruby-version
3
- Gemfile.lock
4
- coverage
data/.rspec DELETED
@@ -1,4 +0,0 @@
1
- --color
2
- --tty
3
- --format documentation
4
- --require spec_helper
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in activerecord-batch_touching.gemspec
4
- gemspec
@@ -1,30 +0,0 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'activerecord/batch_touching/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "activerecord-batch_touching"
8
- spec.version = "1.0-beta" # Activerecord::BatchTouching::VERSION
9
- spec.authors = ["Brian Morearty", "Phil Phillips"]
10
- spec.email = ["phil@productplan.com"]
11
- spec.summary = %q{Batch up your ActiveRecord "touch" operations for better performance.}
12
- spec.description = %q{Batch up your ActiveRecord "touch" operations for better performance. All accumulated "touch" calls will be consolidated into as few database round trips as possible.}
13
- spec.homepage = ""
14
- spec.license = "MIT"
15
-
16
- spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
-
21
- spec.add_dependency "activerecord", ">= 6"
22
-
23
- spec.add_development_dependency "bundler"
24
- spec.add_development_dependency "rake"
25
- spec.add_development_dependency "sqlite3"
26
- spec.add_development_dependency "timecop"
27
- spec.add_development_dependency "rspec-rails"
28
- spec.add_development_dependency "simplecov"
29
- spec.add_development_dependency "simplecov-rcov"
30
- end
@@ -1,369 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Activerecord::BatchTouching do
4
- let!(:owner) { Owner.create name: "Rosey" }
5
- let!(:pet1) { Pet.create(name: "Bones", owner: owner) }
6
- let!(:pet2) { Pet.create(name: "Ema", owner: owner) }
7
- let!(:car) { Car.create(name: "Ferrari", lock_version: 1) }
8
-
9
- it 'has a version number' do
10
- expect(Activerecord::BatchTouching::VERSION).not_to be nil
11
- end
12
-
13
- it "touch returns true when not in a batch_touching block" do
14
- expect(owner.touch).to equal(true)
15
- end
16
-
17
- it "touch returns true in a batch_touching block" do
18
- ActiveRecord::Base.transaction do
19
- expect(owner.touch).to equal(true)
20
- end
21
- end
22
-
23
- it "consolidates touches on a single record when inside a transaction" do
24
- expect_updates [{ "owners" => { ids: owner } }] do
25
- ActiveRecord::Base.transaction do
26
- owner.touch
27
- owner.touch
28
- end
29
- end
30
- end
31
-
32
- it "calls touch callbacks just once when there are multiple touches" do
33
- expect(owner).to receive(:_run_touch_callbacks).once.and_call_original
34
- ActiveRecord::Base.transaction do
35
- owner.touch
36
- owner.touch
37
- end
38
- end
39
-
40
- it 'sets updated_at on the in-memory instance when it eventually touches the record' do
41
- original_time = new_time = nil
42
-
43
- Timecop.freeze(2014, 7, 4, 12, 0, 0) do
44
- original_time = Time.current
45
- owner.touch
46
- end
47
-
48
- Timecop.freeze(2014, 7, 10, 12, 0, 0) do
49
- new_time = Time.current
50
- ActiveRecord::Base.transaction do
51
- owner.touch
52
- expect(owner.updated_at).to eq(original_time)
53
- expect(owner.changed?).to be_falsey
54
- end
55
- end
56
-
57
- expect(owner.updated_at).to eq(new_time)
58
- expect(owner.changed?).to be_falsey
59
- end
60
-
61
- it "does not mark the instance as changed when touch is called" do
62
- ActiveRecord::Base.transaction do
63
- owner.touch
64
- expect(owner).not_to be_changed
65
- end
66
- end
67
-
68
- it "does not mark the instance as changed, even if its lock_version is incremented" do
69
- ActiveRecord::Base.transaction do
70
- car.touch
71
- end
72
- expect(car).not_to be_changed
73
- end
74
-
75
- it "consolidates touches for all instances in a single table" do
76
- expect_updates [{ "pets" => { ids: [pet1, pet2] } }, "owners" => { ids: owner }] do
77
- ActiveRecord::Base.transaction do
78
- pet1.touch
79
- pet2.touch
80
- end
81
- end
82
- end
83
-
84
- it "does nothing if no_touching is on" do
85
- expect(owner).to receive(:_run_touch_callbacks).never
86
- expect_updates [] do
87
- ActiveRecord::Base.no_touching do
88
- ActiveRecord::Base.transaction do
89
- owner.touch
90
- end
91
- end
92
- end
93
- end
94
-
95
- it "only applies touches for which no_touching is off" do
96
- expect(owner).to receive(:_run_touch_callbacks).never
97
- expect(pet1).to receive(:_run_touch_callbacks).once.and_call_original
98
- expect_updates ["pets" => { ids: pet1 }] do
99
- Owner.no_touching do
100
- ActiveRecord::Base.transaction do
101
- owner.touch
102
- pet1.touch
103
- end
104
- end
105
- end
106
- end
107
-
108
- it "does not apply nested touches if no_touching was turned on inside batch_touching" do
109
- expect(owner).to receive(:_run_touch_callbacks).once.and_call_original
110
- expect(pet1).to receive(:_run_touch_callbacks).never
111
- expect_updates ["owners" => { ids: owner }] do
112
- ActiveRecord::Base.transaction do
113
- owner.touch
114
- ActiveRecord::Base.no_touching do
115
- pet1.touch
116
- end
117
- end
118
- end
119
- end
120
-
121
- it "can update nonstandard columns" do
122
- expect_updates ["owners" => { ids: owner, columns: ["updated_at", "happy_at"] }] do
123
- ActiveRecord::Base.transaction do
124
- owner.touch :happy_at
125
- end
126
- end
127
- end
128
-
129
- it "treats string column names and symbol column names as the same" do
130
- expect_updates ["owners" => { ids: owner, columns: ["updated_at", "happy_at"] }] do
131
- ActiveRecord::Base.transaction do
132
- owner.touch :happy_at
133
- owner.touch "happy_at"
134
- end
135
- end
136
- end
137
-
138
- it "splits up nonstandard column touches and standard column touches" do
139
- owner2 = Owner.create name: "Guybrush"
140
-
141
- expect_updates [{ "owners" => { ids: owner, columns: ["updated_at", "happy_at"] } },
142
- { "owners" => { ids: owner2, columns: ["updated_at"] } }] do
143
-
144
- ActiveRecord::Base.transaction do
145
- owner.touch :happy_at
146
- owner2.touch
147
- end
148
- end
149
- end
150
-
151
- it "can update multiple nonstandard columns of a single record in different calls to touch" do
152
- expect_updates [{ "owners" => { ids: owner, columns: ["updated_at", "happy_at"] } },
153
- { "owners" => { ids: owner, columns: ["updated_at", "sad_at"] } }] do
154
-
155
- ActiveRecord::Base.transaction do
156
- owner.touch :happy_at
157
- owner.touch :sad_at
158
- end
159
- end
160
- end
161
-
162
- it "can update multiple nonstandard columns of a single record in a single call to touch" do
163
- expect_updates [{ "owners" => { ids: owner, columns: [ "updated_at", "happy_at", "sad_at"] } }] do
164
-
165
- ActiveRecord::Base.transaction do
166
- owner.touch :happy_at, :sad_at
167
- end
168
- end
169
- end
170
-
171
- it "consolidates touch: true touches" do
172
- expect_updates [{ "pets" => { ids: [pet1, pet2] } }, { "owners" => { ids: owner } }] do
173
- ActiveRecord::Base.transaction do
174
- pet1.touch
175
- pet2.touch
176
- end
177
- end
178
- end
179
-
180
- it "does not touch the owning record via touch: true if it was already touched explicitly" do
181
- expect(owner).to receive(:_run_touch_callbacks).once.and_call_original
182
- expect(pet1).to receive(:_run_touch_callbacks).once.and_call_original
183
- expect(pet2).to receive(:_run_touch_callbacks).once.and_call_original
184
-
185
- expect_updates [{ "pets" => { ids: [pet1, pet2] } }, { "owners" => { ids: owner } }] do
186
- ActiveRecord::Base.transaction do
187
- owner.touch
188
- pet1.touch
189
- pet2.touch
190
- end
191
- end
192
- end
193
-
194
- it "does not consolidate touches when outside a transaction" do
195
- expect_updates [{ "owners" => { ids: owner } },
196
- { "owners" => { ids: owner } }] do
197
- owner.touch
198
- owner.touch
199
- end
200
- end
201
-
202
- it "nested transactions get consolidated into a single set of touches" do
203
- expect(owner).to receive(:_run_touch_callbacks).once.and_call_original
204
- expect(pet1).to receive(:_run_touch_callbacks).once.and_call_original
205
- expect(pet2).to receive(:_run_touch_callbacks).once.and_call_original
206
-
207
- expect_updates [{ "pets" => { ids: [pet1, pet2] } }, { "owners" => { ids: owner } }] do
208
- ActiveRecord::Base.transaction do
209
- pet1.touch
210
- ActiveRecord::Base.transaction do
211
- pet2.touch
212
- end
213
- end
214
- end
215
- end
216
-
217
- it "rolling back from a nested transaction without :requires_new touches the records in the inner transaction" do
218
- expect_updates [{ "pets" => { ids: [pet1, pet2] } }, { "owners" => { ids: owner } }] do
219
- ActiveRecord::Base.transaction do
220
- pet1.touch
221
- ActiveRecord::Base.transaction do
222
- pet2.touch
223
- raise ActiveRecord::Rollback
224
- end
225
- end
226
- end
227
- end
228
-
229
- it "rolling back from a nested transaction with :requires_new does not touch the records in the inner transaction" do
230
- expect_updates [{ "pets" => { ids: pet1 } }, { "owners" => { ids: owner } }] do
231
- ActiveRecord::Base.transaction do
232
- pet1.touch
233
- ActiveRecord::Base.transaction(requires_new: true) do
234
- pet2.touch
235
- raise ActiveRecord::Rollback
236
- end
237
- end
238
- end
239
- end
240
-
241
- it "touching a record in an outer and inner new transaction, then rolling back the inner one, still touches the record" do
242
- expect_updates [{ "pets" => { ids: pet1 } }, { "owners" => { ids: owner } }] do
243
- ActiveRecord::Base.transaction do
244
- pet1.touch
245
- ActiveRecord::Base.transaction(requires_new: true) do
246
- pet1.touch
247
- raise ActiveRecord::Rollback
248
- end
249
- end
250
- end
251
- end
252
-
253
- it "rolling back from an outer transaction does not touch any records" do
254
- expect_updates [] do
255
- ActiveRecord::Base.transaction do
256
- pet1.touch
257
- ActiveRecord::Base.transaction do
258
- pet2.touch :neutered_at
259
- end
260
- raise ActiveRecord::Rollback
261
- end
262
- end
263
- end
264
-
265
- it "consolidates touch: :column_name touches" do
266
- pet_klass = Class.new(ActiveRecord::Base) do
267
- def self.name; 'Pet'; end
268
- belongs_to :owner, :touch => :happy_at
269
- after_touch :after_touch_callback
270
- def after_touch_callback; end
271
- end
272
-
273
- pet = pet_klass.first
274
- owner = pet.owner
275
-
276
- expect_updates [{ "owners" => { ids: owner, columns: ["updated_at", "happy_at"] } }, { "pets" => { ids: pet } }] do
277
- ActiveRecord::Base.transaction do
278
- pet.touch
279
- pet.touch
280
- end
281
- end
282
- end
283
-
284
- it "keeps iterating as long as after_touch keeps causing more records to be touched" do
285
- pet_klass = Class.new(ActiveRecord::Base) do
286
- def self.name; 'Pet'; end
287
- belongs_to :owner
288
-
289
- # Touch the owner in after_touch instead of using touch: true
290
- after_touch :touch_owner
291
- def touch_owner; owner.touch; end
292
- end
293
-
294
- pet = pet_klass.first
295
- owner = pet.owner
296
-
297
- expect_updates [{ "owners" => { ids: owner } }, { "pets" => { ids: pet } }] do
298
- ActiveRecord::Base.transaction do
299
- pet.touch
300
- end
301
- end
302
- end
303
-
304
- it "increments the optimistic lock column in memory and in the DB" do
305
- car1 = Car.create(name: "Ferrari", lock_version: 1)
306
- car2 = Car.create(name: "Lambo", lock_version: 2)
307
-
308
- ActiveRecord::Base.transaction do
309
- car1.touch
310
- car2.touch
311
- end
312
-
313
- expect(car1.lock_version).to equal(2)
314
- expect(car2.lock_version).to equal(3)
315
-
316
- expect(car1.reload.lock_version).to equal(2)
317
- expect(car2.reload.lock_version).to equal(3)
318
- end
319
-
320
- private
321
-
322
- def expect_updates(tables_ids_and_columns)
323
- expected_sql = expected_sql_for(tables_ids_and_columns)
324
- expect(ActiveRecord::Base.connection).to receive(:update).exactly(expected_sql.length).times do |stmt, _, _|
325
- if stmt.to_sql =~ /UPDATE /i
326
- index = expected_sql.index { |expected_stmt| stmt.to_sql =~ expected_stmt }
327
- expect(index).to be, "An unexpected touch occurred: #{stmt.to_sql}"
328
- expected_sql.delete_at(index)
329
- end
330
- end
331
-
332
- yield
333
-
334
- expect(expected_sql).to be_empty, "Some of the expected updates were not executed."
335
- end
336
-
337
- # Creates an array of regular expressions to match the SQL statements that we expect
338
- # to execute.
339
- #
340
- # Each element in the tables_ids_and_columns array is in this form:
341
- #
342
- # { "table_name" => { ids: id_or_array_of_ids, columns: column_name_or_array } }
343
- #
344
- # 'columns' is optional. If it's missing it is assumed that "updated_at" is the only
345
- # column that gets touched.
346
- def expected_sql_for(tables_ids_and_columns)
347
- tables_ids_and_columns.map do |entry|
348
- entry.map do |table, options|
349
- ids = Array.wrap(options[:ids])
350
- columns = Array.wrap(options[:columns]).presence || ["updated_at"]
351
- columns = columns.sort
352
- Regexp.new(touch_sql(table, columns, ids))
353
- end
354
- end.flatten
355
- end
356
-
357
- # in: array of records or record ids
358
- # out: "( = 1|= \?|= \$1)" or " IN (1, 2)"
359
- #
360
- # In some cases, such as SQLite3 when outside a transaction, the logged SQL uses ? instead of record ids.
361
- def ids_sql(ids)
362
- ids = ids.map { |id| id.class.respond_to?(:primary_key) ? id.send(id.class.primary_key) : id }
363
- ids.length > 1 ? %{ IN \\(#{Array.new(ids.length, '\?').join(", ")}\\)} : %{( = #{ids.first}|= \\?|= \\$1)}
364
- end
365
-
366
- def touch_sql(table, columns, ids)
367
- %{UPDATE \\"#{table}"\\ SET #{columns.map { |column| %{\\"#{column}\\" =.+} }.join(", ") } .+#{ids_sql(ids)}\\Z}
368
- end
369
- end
@@ -1,3 +0,0 @@
1
- @exclude_list = [
2
- 'spec/**/*.rb'
3
- ]
data/spec/spec_helper.rb DELETED
@@ -1,22 +0,0 @@
1
- if ENV["COVERAGE"]
2
- require_relative 'rcov_exclude_list.rb'
3
- exlist = Dir.glob(@exclude_list)
4
- require 'simplecov'
5
- require 'simplecov-rcov'
6
- SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
7
- SimpleCov.start do
8
- exlist.each do |p|
9
- add_filter p
10
- end
11
- end
12
- end
13
-
14
- $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
15
- require 'active_record'
16
- require 'activerecord/batch_touching'
17
- require 'timecop'
18
-
19
- ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
20
-
21
- load File.dirname(__FILE__) + '/support/schema.rb'
22
- require File.dirname(__FILE__) + '/support/models.rb'
@@ -1,9 +0,0 @@
1
- class Owner < ActiveRecord::Base
2
- has_many :pets, inverse_of: :owner
3
- end
4
-
5
- class Pet < ActiveRecord::Base
6
- belongs_to :owner, touch: true, inverse_of: :pets
7
- end
8
-
9
- class Car < ActiveRecord::Base; end
@@ -1,27 +0,0 @@
1
- ActiveRecord::Schema.define do
2
- self.verbose = false
3
-
4
- create_table :owners, :force => true do |t|
5
- t.string :name
6
-
7
- t.timestamps
8
- t.datetime :happy_at
9
- t.datetime :sad_at
10
- end
11
-
12
- create_table :pets, :force => true do |t|
13
- t.string :name
14
- t.integer :owner_id
15
- t.datetime :neutered_at
16
-
17
- t.timestamps
18
- end
19
-
20
- create_table :cars, force: true do |t|
21
- t.string :name
22
- t.column :lock_version, :integer, null: false, default: 0
23
-
24
- t.timestamps
25
- end
26
-
27
- end