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

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 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