paper_trail 10.2.0 → 10.2.1

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
- SHA1:
3
- metadata.gz: 9a180f82e731345bd4b9f46971ee6ca61753f82e
4
- data.tar.gz: c32cde31e4da02b078d13c8c2667a6b0c49c4110
2
+ SHA256:
3
+ metadata.gz: 35e835ccf1aa917cfbe1202f639d73f7e70f4f35e4845706f6edb8ba713004b3
4
+ data.tar.gz: eafb8a0ed8ddfc6daeafbd7fb8197c225a2a962a1e32f42664802b72f386d510
5
5
  SHA512:
6
- metadata.gz: bd84d2586c73b9f08e44fabb1298a6e530ecf25953da0a05a41a925870720e6c05eb74474321816b51dd24cceb1d14b0658f685ac0c68a91de3ff6e7c2e0525b
7
- data.tar.gz: fb8e6814903909666d842d43b68b5ef97e5c207a8193d34a5d199f8405fb83565c87ba86925c58148720debde25bdd442f712ef75b9acaf8b3d86a6c71978191
6
+ metadata.gz: 2c22225eb860afba59822dac469c24bd6aa3fd9712a521a93187ce0ce497a2c66a022c8cb6615d1cfc13fbfc8ac9df386cf4f038ac1ec7fd78cea9611d86f26e
7
+ data.tar.gz: efb345a2262034ee6ac5366bc1825bb710b79ed3b80e99567bfe873cc1aca7815e8f5136e066358d542ad52aa777274059c3334d5b6fb56c4fff24ecc4521b65
@@ -59,14 +59,29 @@ module PaperTrail
59
59
  end
60
60
 
61
61
  # @api private
62
- def attributes_before_change(is_touch)
63
- Hash[@record.attributes.map do |k, v|
64
- if @record.class.column_names.include?(k)
65
- [k, attribute_in_previous_version(k, is_touch)]
66
- else
67
- [k, v]
62
+ def nonskipped_attributes_before_change(is_touch)
63
+ cache_changed_attributes do
64
+ record_attributes = @record.attributes.except(*@record.paper_trail_options[:skip])
65
+
66
+ record_attributes.each_key do |k|
67
+ if @record.class.column_names.include?(k)
68
+ record_attributes[k] = attribute_in_previous_version(k, is_touch)
69
+ end
68
70
  end
69
- end]
71
+ end
72
+ end
73
+
74
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`.
75
+ # @api private
76
+ def cache_changed_attributes
77
+ if RAILS_GTE_5_1
78
+ # Everything works fine as it is
79
+ yield
80
+ else
81
+ # Any particular call to `changed_attributes` produces the huge memory allocation.
82
+ # Lets use the generic AR workaround for that.
83
+ @record.send(:cache_changed_attributes) { yield }
84
+ end
70
85
  end
71
86
 
72
87
  # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
@@ -108,7 +123,8 @@ module PaperTrail
108
123
 
109
124
  # @api private
110
125
  def changed_in_latest_version
111
- changes_in_latest_version.keys
126
+ # Memoized to reduce memory usage
127
+ @changed_in_latest_version ||= changes_in_latest_version.keys
112
128
  end
113
129
 
114
130
  # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
@@ -116,10 +132,13 @@ module PaperTrail
116
132
  #
117
133
  # @api private
118
134
  def changes_in_latest_version
119
- if @in_after_callback && RAILS_GTE_5_1
120
- @record.saved_changes
121
- else
122
- @record.changes
135
+ # Memoized to reduce memory usage
136
+ @changes_in_latest_version ||= begin
137
+ if @in_after_callback && RAILS_GTE_5_1
138
+ @record.saved_changes
139
+ else
140
+ @record.changes
141
+ end
123
142
  end
124
143
  end
125
144
 
@@ -200,16 +219,19 @@ module PaperTrail
200
219
 
201
220
  # @api private
202
221
  def notably_changed
203
- only = @record.paper_trail_options[:only].dup
204
- # Remove Hash arguments and then evaluate whether the attributes (the
205
- # keys of the hash) should also get pushed into the collection.
206
- only.delete_if do |obj|
207
- obj.is_a?(Hash) &&
208
- obj.each { |attr, condition|
209
- only << attr if condition.respond_to?(:call) && condition.call(@record)
210
- }
222
+ # Memoized to reduce memory usage
223
+ @notably_changed ||= begin
224
+ only = @record.paper_trail_options[:only].dup
225
+ # Remove Hash arguments and then evaluate whether the attributes (the
226
+ # keys of the hash) should also get pushed into the collection.
227
+ only.delete_if do |obj|
228
+ obj.is_a?(Hash) &&
229
+ obj.each { |attr, condition|
230
+ only << attr if condition.respond_to?(:call) && condition.call(@record)
231
+ }
232
+ end
233
+ only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
211
234
  end
212
- only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
213
235
  end
214
236
 
215
237
  # Returns hash of attributes (with appropriate attributes serialized),
@@ -217,8 +239,7 @@ module PaperTrail
217
239
  #
218
240
  # @api private
219
241
  def object_attrs_for_paper_trail(is_touch)
220
- attrs = attributes_before_change(is_touch).
221
- except(*@record.paper_trail_options[:skip])
242
+ attrs = nonskipped_attributes_before_change(is_touch)
222
243
  AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs)
223
244
  attrs
224
245
  end
@@ -237,9 +258,14 @@ module PaperTrail
237
258
  # serialization here, using `PaperTrail.serializer`.
238
259
  #
239
260
  # @api private
261
+ # @param changes HashWithIndifferentAccess
240
262
  def recordable_object_changes(changes)
241
263
  if PaperTrail.config.object_changes_adapter&.respond_to?(:diff)
242
- changes = PaperTrail.config.object_changes_adapter.diff(changes)
264
+ # We'd like to avoid the `to_hash` here, because it increases memory
265
+ # usage, but that would be a breaking change because
266
+ # `object_changes_adapter` expects a plain `Hash`, not a
267
+ # `HashWithIndifferentAccess`.
268
+ changes = PaperTrail.config.object_changes_adapter.diff(changes.to_hash)
243
269
  end
244
270
 
245
271
  if @record.class.paper_trail.version_class.object_changes_col_is_json?
@@ -284,7 +310,10 @@ module PaperTrail
284
310
  AttributeSerializers::ObjectChangesAttribute.
285
311
  new(@record.class).
286
312
  serialize(changes)
287
- changes.to_hash
313
+
314
+ # We'd like to convert this `HashWithIndifferentAccess` to a plain
315
+ # `Hash`, but we don't, to save memory.
316
+ changes
288
317
  end
289
318
  end
290
319
  end
@@ -13,6 +13,7 @@ module PaperTrail
13
13
  # @api private
14
14
  def data
15
15
  data = {
16
+ item: @record,
16
17
  event: @record.paper_trail_event || "create",
17
18
  whodunnit: PaperTrail.request.whodunnit
18
19
  }
@@ -25,6 +25,7 @@ module PaperTrail
25
25
  # @api private
26
26
  def data
27
27
  data = {
28
+ item: @record,
28
29
  event: @record.paper_trail_event || "update",
29
30
  whodunnit: PaperTrail.request.whodunnit
30
31
  }
@@ -67,14 +67,14 @@ module PaperTrail
67
67
 
68
68
  def record_create
69
69
  return unless enabled?
70
- event = Events::Create.new(@record, true)
71
70
 
72
- # Merge data from `Event` with data from PT-AT. We no longer use
73
- # `data_for_create` but PT-AT still does.
74
- data = event.data.merge(data_for_create)
75
-
76
- versions_assoc = @record.send(@record.class.versions_association_name)
77
- versions_assoc.create!(data)
71
+ build_version_on_create(in_after_callback: true).tap do |version|
72
+ version.save!
73
+ # Because the version object was created using version_class.new instead
74
+ # of versions_assoc.build?, the association cache is unaware. So, we
75
+ # invalidate the `versions` association cache with `reset`.
76
+ versions.reset
77
+ end
78
78
  end
79
79
 
80
80
  # PT-AT extends this method to add its transaction id.
@@ -119,19 +119,22 @@ module PaperTrail
119
119
  # paper_trail-association_tracking
120
120
  def record_update(force:, in_after_callback:, is_touch:)
121
121
  return unless enabled?
122
- event = Events::Update.new(@record, in_after_callback, is_touch, nil)
123
- return unless force || event.changed_notably?
124
122
 
125
- # Merge data from `Event` with data from PT-AT. We no longer use
126
- # `data_for_update` but PT-AT still does.
127
- data = event.data.merge(data_for_update)
123
+ version = build_version_on_update(
124
+ force: force,
125
+ in_after_callback: in_after_callback,
126
+ is_touch: is_touch
127
+ )
128
+ return unless version
128
129
 
129
- versions_assoc = @record.send(@record.class.versions_association_name)
130
- version = versions_assoc.create(data)
131
- if version.errors.any?
132
- log_version_errors(version, :update)
133
- else
130
+ if version.save
131
+ # Because the version object was created using version_class.new instead
132
+ # of versions_assoc.build?, the association cache is unaware. So, we
133
+ # invalidate the `versions` association cache with `reset`.
134
+ versions.reset
134
135
  version
136
+ else
137
+ log_version_errors(version, :update)
135
138
  end
136
139
  end
137
140
 
@@ -250,6 +253,35 @@ module PaperTrail
250
253
  @record.send(@record.class.versions_association_name).reset
251
254
  end
252
255
 
256
+ # @api private
257
+ def build_version_on_create(in_after_callback:)
258
+ event = Events::Create.new(@record, in_after_callback)
259
+
260
+ # Merge data from `Event` with data from PT-AT. We no longer use
261
+ # `data_for_create` but PT-AT still does.
262
+ data = event.data.merge!(data_for_create)
263
+
264
+ # Pure `version_class.new` reduces memory usage compared to `versions_assoc.build`
265
+ @record.class.paper_trail.version_class.new(data)
266
+ end
267
+
268
+ # @api private
269
+ def build_version_on_update(force:, in_after_callback:, is_touch:)
270
+ event = Events::Update.new(@record, in_after_callback, is_touch, nil)
271
+ return unless force || event.changed_notably?
272
+
273
+ # Merge data from `Event` with data from PT-AT. We no longer use
274
+ # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
275
+ # instead of `merge`.
276
+ data = event.data.merge!(data_for_update)
277
+
278
+ # Using `version_class.new` reduces memory usage compared to
279
+ # `versions_assoc.build`. It's a trade-off though. We have to clear
280
+ # the association cache (see `versions.reset`) and that could cause an
281
+ # additional query in certain applications.
282
+ @record.class.paper_trail.version_class.new(data)
283
+ end
284
+
253
285
  def log_version_errors(version, action)
254
286
  version.logger&.warn(
255
287
  "Unable to create version for #{action} of #{@record.class.name}" \
@@ -12,7 +12,12 @@ module PaperTrail
12
12
  ::YAML.load string
13
13
  end
14
14
 
15
+ # @param object (Hash | HashWithIndifferentAccess) - Coming from
16
+ # `recordable_object` `object` will be a plain `Hash`. However, due to
17
+ # recent [memory optimizations](https://git.io/fjeYv), when coming from
18
+ # `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
15
19
  def dump(object)
20
+ object = object.to_hash if object.is_a?(HashWithIndifferentAccess)
16
21
  ::YAML.dump object
17
22
  end
18
23
 
@@ -9,7 +9,7 @@ module PaperTrail
9
9
  module VERSION
10
10
  MAJOR = 10
11
11
  MINOR = 2
12
- TINY = 0
12
+ TINY = 1
13
13
 
14
14
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
15
15
  PRE = nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paper_trail
3
3
  version: !ruby/object:Gem::Version
4
- version: 10.2.0
4
+ version: 10.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Stewart
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2019-01-31 00:00:00.000000000 Z
13
+ date: 2019-03-14 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -102,6 +102,20 @@ dependencies:
102
102
  - - "~>"
103
103
  - !ruby/object:Gem::Version
104
104
  version: 0.9.4
105
+ - !ruby/object:Gem::Dependency
106
+ name: memory_profiler
107
+ requirement: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: 0.9.12
112
+ type: :development
113
+ prerelease: false
114
+ version_requirements: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: 0.9.12
105
119
  - !ruby/object:Gem::Dependency
106
120
  name: mysql2
107
121
  requirement: !ruby/object:Gem::Requirement
@@ -206,14 +220,14 @@ dependencies:
206
220
  requirements:
207
221
  - - "~>"
208
222
  - !ruby/object:Gem::Version
209
- version: '1.3'
223
+ version: 1.3.13
210
224
  type: :development
211
225
  prerelease: false
212
226
  version_requirements: !ruby/object:Gem::Requirement
213
227
  requirements:
214
228
  - - "~>"
215
229
  - !ruby/object:Gem::Version
216
- version: '1.3'
230
+ version: 1.3.13
217
231
  description: |
218
232
  Track changes to your models, for auditing or versioning. See how a model looked
219
233
  at any stage in its lifecycle, revert it to any version, or restore it after it
@@ -284,8 +298,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
284
298
  - !ruby/object:Gem::Version
285
299
  version: 1.3.6
286
300
  requirements: []
287
- rubyforge_project:
288
- rubygems_version: 2.5.2.3
301
+ rubygems_version: 3.0.3
289
302
  signing_key:
290
303
  specification_version: 4
291
304
  summary: Track changes to your models.