historiographer 4.4.4 → 4.4.5
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 +4 -4
- data/README.md +54 -3
- data/VERSION +1 -1
- data/historiographer.gemspec +3 -3
- data/lib/historiographer/history.rb +4 -1
- data/lib/historiographer.rb +8 -8
- data/spec/examples.txt +60 -59
- data/spec/historiographer_spec.rb +30 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 99a33a798a97f440f2d4c0d2d1973303a013e99bb5f8d3f566c51a0d9375961f
|
|
4
|
+
data.tar.gz: 56b51522b7f4b254b9ba1e4e1813aa449dec80579e9e79c8e4f446e629ce74c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d27ffa3d2aaca0a91d9283c5f18701e7c576475d2619c569dd7edc21cdc8983c3376e823067ffb233a1f14ec4123823564b486c906da6b7860c285eb555ecc67
|
|
7
|
+
data.tar.gz: 7b1608e2c572ab02da5e2371679fe5facc463ac4d788e1e9e499a2078be074e3345758e9a9aed97931ade4c15f59fd220d2f8957e7d7101f44290217647a9ac2
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The Audited gem has some serious flaws.
|
|
|
12
12
|
|
|
13
13
|
2. It doesn't provide the indexes you need from your primary tables
|
|
14
14
|
|
|
15
|
-
3. It doesn't
|
|
15
|
+
3. It doesn't provide out-of-the-box snapshots
|
|
16
16
|
|
|
17
17
|
## How does Historiographer solve these problems?
|
|
18
18
|
|
|
@@ -86,14 +86,17 @@ You can take a snapshot of a record and all its associated records:
|
|
|
86
86
|
|
|
87
87
|
```ruby
|
|
88
88
|
post = Post.find(1)
|
|
89
|
-
post.snapshot
|
|
89
|
+
post.snapshot
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
This will:
|
|
93
93
|
|
|
94
94
|
1. Create a history record for the post
|
|
95
|
-
2.
|
|
95
|
+
2. Recursively create history records for all associated records that also include Historiographer (comments, author, etc.)
|
|
96
96
|
3. Link these history records together with a shared `snapshot_id`
|
|
97
|
+
4. Skip any view-backed models (models without a primary key)
|
|
98
|
+
|
|
99
|
+
**Note:** The snapshot cascades to ALL associations where the associated model includes Historiographer. There is currently no built-in way to limit which associations get snapshotted. If you need finer control, you can either not include Historiographer on models you don't want snapshotted, or override the `snapshot` method.
|
|
97
100
|
|
|
98
101
|
You can retrieve the latest snapshot using:
|
|
99
102
|
|
|
@@ -130,6 +133,38 @@ This can be useful when:
|
|
|
130
133
|
- You're versioning training data for machine learning models
|
|
131
134
|
- You need to maintain immutable audit trails at specific checkpoints
|
|
132
135
|
|
|
136
|
+
## Safe and Silent Modes
|
|
137
|
+
|
|
138
|
+
Historiographer provides two additional modes for migrating existing models:
|
|
139
|
+
|
|
140
|
+
### Historiographer::Safe
|
|
141
|
+
|
|
142
|
+
Use `Historiographer::Safe` when migrating an existing model to Historiographer. Instead of raising an error when `history_user_id` is missing, it logs to Rollbar, allowing you to find all locations that need to be updated:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class Post < ActiveRecord::Base
|
|
146
|
+
include Historiographer::Safe
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# This will create a history record and log to Rollbar (instead of raising an error)
|
|
150
|
+
Post.create(title: "My Post")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Historiographer::Silent
|
|
154
|
+
|
|
155
|
+
Use `Historiographer::Silent` when you want to allow missing `history_user_id` without any errors or logging:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
class Post < ActiveRecord::Base
|
|
159
|
+
include Historiographer::Silent
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# This will create a history record silently without history_user_id
|
|
163
|
+
Post.create(title: "My Post")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Note:** Both Safe and Silent modes are intended for migration purposes, not as long-term solutions.
|
|
167
|
+
|
|
133
168
|
## Namespaced Models
|
|
134
169
|
|
|
135
170
|
When using namespaced models, Rails handles foreign key naming differently than with non-namespaced models. For example, if you have a model namespaced like this:
|
|
@@ -296,6 +331,7 @@ You should also make a `PostHistory` class if you're going to query `PostHistory
|
|
|
296
331
|
```ruby
|
|
297
332
|
class PostHistory < ActiveRecord::Base
|
|
298
333
|
self.table_name = "post_histories"
|
|
334
|
+
include Historiographer::History
|
|
299
335
|
end
|
|
300
336
|
```
|
|
301
337
|
|
|
@@ -323,6 +359,21 @@ Post.last.destroy!(history_user_id: current_user.id)
|
|
|
323
359
|
Post.destroy_all(history_user_id: current_user.id)
|
|
324
360
|
```
|
|
325
361
|
|
|
362
|
+
### Skipping History
|
|
363
|
+
|
|
364
|
+
If you need to save a record without creating a history entry:
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
post = Post.new(title: "My Post")
|
|
368
|
+
post.save_without_history # No history_user_id required
|
|
369
|
+
post.save_without_history! # Bang version
|
|
370
|
+
|
|
371
|
+
# For bulk operations:
|
|
372
|
+
Post.all.update_all_without_history(title: "New Title")
|
|
373
|
+
Post.all.delete_all_without_history
|
|
374
|
+
Post.all.destroy_all_without_history
|
|
375
|
+
```
|
|
376
|
+
|
|
326
377
|
The `histories` classes have a `current` method, which only finds current history records. These records will also be the same as the data in the primary table.
|
|
327
378
|
|
|
328
379
|
```ruby
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.4.
|
|
1
|
+
4.4.5
|
data/historiographer.gemspec
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
|
5
|
-
# stub: historiographer 4.4.
|
|
5
|
+
# stub: historiographer 4.4.5 ruby lib
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |s|
|
|
8
8
|
s.name = "historiographer".freeze
|
|
9
|
-
s.version = "4.4.
|
|
9
|
+
s.version = "4.4.5"
|
|
10
10
|
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
12
12
|
s.require_paths = ["lib".freeze]
|
|
13
13
|
s.authors = ["brettshollenberger".freeze]
|
|
14
|
-
s.date = "
|
|
14
|
+
s.date = "2026-01-14"
|
|
15
15
|
s.description = "Creates separate tables for each history table".freeze
|
|
16
16
|
s.email = "brett.shollenberger@gmail.com".freeze
|
|
17
17
|
s.executables = ["console".freeze, "setup".freeze, "test".freeze, "test-all".freeze, "test-rails".freeze]
|
|
@@ -109,8 +109,11 @@ module Historiographer
|
|
|
109
109
|
#
|
|
110
110
|
# Set up user association unless Silent module is included
|
|
111
111
|
# Defer this check until foreign_class is available
|
|
112
|
+
# Use optional: true because history_user_id validation is handled separately
|
|
113
|
+
# by Historiographer's validate_history_user_id_present method, and we need
|
|
114
|
+
# to allow updates without a user when using without_history_user_id blocks
|
|
112
115
|
unless base.foreign_class && base.foreign_class.ancestors.include?(Historiographer::Silent)
|
|
113
|
-
belongs_to :user, foreign_key: :history_user_id
|
|
116
|
+
belongs_to :user, foreign_key: :history_user_id, optional: true
|
|
114
117
|
end
|
|
115
118
|
|
|
116
119
|
# Add method_added hook to the original class when it's available
|
data/lib/historiographer.rb
CHANGED
|
@@ -188,7 +188,7 @@ module Historiographer
|
|
|
188
188
|
rescue NameError
|
|
189
189
|
# Get the base table name without _histories suffix
|
|
190
190
|
base_table = base.table_name.singularize.sub(/_histories$/, '')
|
|
191
|
-
|
|
191
|
+
|
|
192
192
|
history_class_initializer = Class.new(ActiveRecord::Base) do
|
|
193
193
|
self.table_name = "#{base_table}_histories"
|
|
194
194
|
end
|
|
@@ -204,7 +204,7 @@ module Historiographer
|
|
|
204
204
|
|
|
205
205
|
# Set the constant in the correct module
|
|
206
206
|
history_class = target_module.const_set(final_class_name, history_class_initializer)
|
|
207
|
-
|
|
207
|
+
|
|
208
208
|
# Now that the class is named, include the History module and extend class methods
|
|
209
209
|
history_class.send(:include, Historiographer::History)
|
|
210
210
|
end
|
|
@@ -279,7 +279,7 @@ module Historiographer
|
|
|
279
279
|
save!(*args, &block)
|
|
280
280
|
@no_history = false
|
|
281
281
|
end
|
|
282
|
-
|
|
282
|
+
|
|
283
283
|
def snapshot(tree = {}, snapshot_id = nil)
|
|
284
284
|
return if is_history_class?
|
|
285
285
|
|
|
@@ -296,7 +296,7 @@ module Historiographer
|
|
|
296
296
|
null_snapshot = history_class.where(foreign_key => attrs[primary_key], snapshot_id: nil).first
|
|
297
297
|
snapshot = nil
|
|
298
298
|
if null_snapshot.present?
|
|
299
|
-
null_snapshot.update(snapshot_id: snapshot_id)
|
|
299
|
+
null_snapshot.update!(snapshot_id: snapshot_id)
|
|
300
300
|
snapshot = null_snapshot
|
|
301
301
|
else
|
|
302
302
|
snapshot = record_history(snapshot_id: snapshot_id)
|
|
@@ -308,7 +308,7 @@ module Historiographer
|
|
|
308
308
|
association_class = association.klass rescue nil
|
|
309
309
|
next if association_class.nil?
|
|
310
310
|
next if association_class.primary_key.nil?
|
|
311
|
-
|
|
311
|
+
|
|
312
312
|
associated_records = send(association.name)&.reload
|
|
313
313
|
if associated_records.respond_to?(:order)
|
|
314
314
|
associated_records = associated_records.order(id: :asc)
|
|
@@ -383,7 +383,7 @@ module Historiographer
|
|
|
383
383
|
|
|
384
384
|
if history_class.history_foreign_key.present? && history_class.present?
|
|
385
385
|
result = history_class.insert_all([attrs])
|
|
386
|
-
|
|
386
|
+
|
|
387
387
|
# Check if the insertion was successful
|
|
388
388
|
if result.rows.empty?
|
|
389
389
|
# insert_all returned empty rows, likely due to a duplicate/conflict
|
|
@@ -393,7 +393,7 @@ module Historiographer
|
|
|
393
393
|
foreign_key => attrs[foreign_key],
|
|
394
394
|
history_started_at: attrs['history_started_at']
|
|
395
395
|
).first
|
|
396
|
-
|
|
396
|
+
|
|
397
397
|
if existing_history
|
|
398
398
|
# A duplicate history already exists (race condition or retry)
|
|
399
399
|
# This is acceptable - return the existing history
|
|
@@ -404,7 +404,7 @@ module Historiographer
|
|
|
404
404
|
history_class.create!(attrs) # This will raise the correct error since it will fail the unique constraint
|
|
405
405
|
end
|
|
406
406
|
end
|
|
407
|
-
|
|
407
|
+
|
|
408
408
|
inserted_id = result.rows.first.first if history_class.primary_key == 'id'
|
|
409
409
|
instance = history_class.find(inserted_id)
|
|
410
410
|
current_history.update_columns(history_ended_at: now) if current_history.present?
|
data/spec/examples.txt
CHANGED
|
@@ -1,64 +1,65 @@
|
|
|
1
1
|
example_id | status | run_time |
|
|
2
2
|
------------------------------------------------------------------ | ------ | --------------- |
|
|
3
|
-
./spec/historiographer_spec.rb[1:1:1] | passed | 0.
|
|
4
|
-
./spec/historiographer_spec.rb[1:1:2] | passed | 0.
|
|
5
|
-
./spec/historiographer_spec.rb[1:1:3] | passed | 0.
|
|
6
|
-
./spec/historiographer_spec.rb[1:2:1] | passed | 0.
|
|
7
|
-
./spec/historiographer_spec.rb[1:2:2] | passed | 0.
|
|
8
|
-
./spec/historiographer_spec.rb[1:2:3:1:1] | passed | 0.
|
|
9
|
-
./spec/historiographer_spec.rb[1:2:3:1:2] | passed | 0.
|
|
10
|
-
./spec/historiographer_spec.rb[1:2:3:2:1] | passed | 0.
|
|
11
|
-
./spec/historiographer_spec.rb[1:2:3:2:2] | passed | 0.
|
|
12
|
-
./spec/historiographer_spec.rb[1:2:3:2:3] | passed | 0.
|
|
13
|
-
./spec/historiographer_spec.rb[1:2:3:3:1] | passed | 0.
|
|
14
|
-
./spec/historiographer_spec.rb[1:2:3:3:2] | passed | 0.
|
|
15
|
-
./spec/historiographer_spec.rb[1:2:4:1] | passed | 0.
|
|
16
|
-
./spec/historiographer_spec.rb[1:2:4:2] | passed | 0.
|
|
17
|
-
./spec/historiographer_spec.rb[1:2:4:3] | passed | 0.
|
|
18
|
-
./spec/historiographer_spec.rb[1:2:5:1] | passed | 0.
|
|
19
|
-
./spec/historiographer_spec.rb[1:2:5:2] | passed | 0.
|
|
20
|
-
./spec/historiographer_spec.rb[1:2:5:3] | passed | 0.
|
|
21
|
-
./spec/historiographer_spec.rb[1:2:6] | passed | 0.
|
|
22
|
-
./spec/historiographer_spec.rb[1:2:7] | passed | 0.
|
|
23
|
-
./spec/historiographer_spec.rb[1:2:8] | passed | 0.
|
|
24
|
-
./spec/historiographer_spec.rb[1:3:1] | passed | 0.
|
|
25
|
-
./spec/historiographer_spec.rb[1:4:1] | passed | 0.
|
|
26
|
-
./spec/historiographer_spec.rb[1:5:1] | passed | 0.
|
|
27
|
-
./spec/historiographer_spec.rb[1:5:2] | passed | 0.
|
|
28
|
-
./spec/historiographer_spec.rb[1:6:1] | passed | 0.
|
|
29
|
-
./spec/historiographer_spec.rb[1:6:2] | passed | 0.
|
|
30
|
-
./spec/historiographer_spec.rb[1:7:1] | passed | 0.
|
|
31
|
-
./spec/historiographer_spec.rb[1:7:2] | passed | 0.
|
|
32
|
-
./spec/historiographer_spec.rb[1:7:3] | passed | 0.
|
|
33
|
-
./spec/historiographer_spec.rb[1:7:4] | passed | 0.
|
|
34
|
-
./spec/historiographer_spec.rb[1:7:5] | passed | 0.
|
|
35
|
-
./spec/historiographer_spec.rb[1:8:1] | passed | 0.
|
|
36
|
-
./spec/historiographer_spec.rb[1:9:1] | passed | 0.
|
|
37
|
-
./spec/historiographer_spec.rb[1:10:1] | passed | 0.
|
|
38
|
-
./spec/historiographer_spec.rb[1:11:1] | passed | 0.
|
|
39
|
-
./spec/historiographer_spec.rb[1:11:2] | passed | 0.
|
|
40
|
-
./spec/historiographer_spec.rb[1:11:3] | passed | 0.
|
|
41
|
-
./spec/historiographer_spec.rb[1:11:4] | passed | 0.
|
|
42
|
-
./spec/historiographer_spec.rb[1:12:1] | passed | 0.
|
|
43
|
-
./spec/historiographer_spec.rb[1:12:2] | passed | 0.
|
|
44
|
-
./spec/historiographer_spec.rb[1:12:3] | passed | 0.
|
|
45
|
-
./spec/historiographer_spec.rb[1:12:4] | passed | 0.
|
|
46
|
-
./spec/historiographer_spec.rb[1:12:5] | passed | 0.
|
|
47
|
-
./spec/historiographer_spec.rb[1:12:6] | passed | 0.
|
|
48
|
-
./spec/historiographer_spec.rb[1:
|
|
49
|
-
./spec/historiographer_spec.rb[1:
|
|
50
|
-
./spec/historiographer_spec.rb[1:14:
|
|
51
|
-
./spec/historiographer_spec.rb[1:14:
|
|
52
|
-
./spec/historiographer_spec.rb[1:14:
|
|
53
|
-
./spec/historiographer_spec.rb[1:
|
|
54
|
-
./spec/historiographer_spec.rb[1:15:
|
|
55
|
-
./spec/historiographer_spec.rb[1:
|
|
56
|
-
./spec/historiographer_spec.rb[1:16:
|
|
57
|
-
./spec/historiographer_spec.rb[1:16:
|
|
58
|
-
./spec/historiographer_spec.rb[1:16:
|
|
59
|
-
./spec/historiographer_spec.rb[1:
|
|
60
|
-
./spec/historiographer_spec.rb[1:17:1:
|
|
61
|
-
./spec/historiographer_spec.rb[1:17:2
|
|
3
|
+
./spec/historiographer_spec.rb[1:1:1] | passed | 0.07846 seconds |
|
|
4
|
+
./spec/historiographer_spec.rb[1:1:2] | passed | 0.05421 seconds |
|
|
5
|
+
./spec/historiographer_spec.rb[1:1:3] | passed | 0.05161 seconds |
|
|
6
|
+
./spec/historiographer_spec.rb[1:2:1] | passed | 0.05923 seconds |
|
|
7
|
+
./spec/historiographer_spec.rb[1:2:2] | passed | 0.0649 seconds |
|
|
8
|
+
./spec/historiographer_spec.rb[1:2:3:1:1] | passed | 0.10678 seconds |
|
|
9
|
+
./spec/historiographer_spec.rb[1:2:3:1:2] | passed | 0.07362 seconds |
|
|
10
|
+
./spec/historiographer_spec.rb[1:2:3:2:1] | passed | 0.0583 seconds |
|
|
11
|
+
./spec/historiographer_spec.rb[1:2:3:2:2] | passed | 0.06499 seconds |
|
|
12
|
+
./spec/historiographer_spec.rb[1:2:3:2:3] | passed | 0.07272 seconds |
|
|
13
|
+
./spec/historiographer_spec.rb[1:2:3:3:1] | passed | 0.07946 seconds |
|
|
14
|
+
./spec/historiographer_spec.rb[1:2:3:3:2] | passed | 0.0671 seconds |
|
|
15
|
+
./spec/historiographer_spec.rb[1:2:4:1] | passed | 0.06979 seconds |
|
|
16
|
+
./spec/historiographer_spec.rb[1:2:4:2] | passed | 0.06026 seconds |
|
|
17
|
+
./spec/historiographer_spec.rb[1:2:4:3] | passed | 0.05019 seconds |
|
|
18
|
+
./spec/historiographer_spec.rb[1:2:5:1] | passed | 0.0693 seconds |
|
|
19
|
+
./spec/historiographer_spec.rb[1:2:5:2] | passed | 0.06955 seconds |
|
|
20
|
+
./spec/historiographer_spec.rb[1:2:5:3] | passed | 0.05246 seconds |
|
|
21
|
+
./spec/historiographer_spec.rb[1:2:6] | passed | 0.04995 seconds |
|
|
22
|
+
./spec/historiographer_spec.rb[1:2:7] | passed | 0.05019 seconds |
|
|
23
|
+
./spec/historiographer_spec.rb[1:2:8] | passed | 0.05534 seconds |
|
|
24
|
+
./spec/historiographer_spec.rb[1:3:1] | passed | 0.05769 seconds |
|
|
25
|
+
./spec/historiographer_spec.rb[1:4:1] | passed | 0.07179 seconds |
|
|
26
|
+
./spec/historiographer_spec.rb[1:5:1] | passed | 0.05631 seconds |
|
|
27
|
+
./spec/historiographer_spec.rb[1:5:2] | passed | 0.0544 seconds |
|
|
28
|
+
./spec/historiographer_spec.rb[1:6:1] | passed | 0.05814 seconds |
|
|
29
|
+
./spec/historiographer_spec.rb[1:6:2] | passed | 0.08519 seconds |
|
|
30
|
+
./spec/historiographer_spec.rb[1:7:1] | passed | 0.0567 seconds |
|
|
31
|
+
./spec/historiographer_spec.rb[1:7:2] | passed | 0.0759 seconds |
|
|
32
|
+
./spec/historiographer_spec.rb[1:7:3] | passed | 0.05536 seconds |
|
|
33
|
+
./spec/historiographer_spec.rb[1:7:4] | passed | 0.05564 seconds |
|
|
34
|
+
./spec/historiographer_spec.rb[1:7:5] | passed | 0.07313 seconds |
|
|
35
|
+
./spec/historiographer_spec.rb[1:8:1] | passed | 0.05466 seconds |
|
|
36
|
+
./spec/historiographer_spec.rb[1:9:1] | passed | 0.07755 seconds |
|
|
37
|
+
./spec/historiographer_spec.rb[1:10:1] | passed | 0.05444 seconds |
|
|
38
|
+
./spec/historiographer_spec.rb[1:11:1] | passed | 0.05663 seconds |
|
|
39
|
+
./spec/historiographer_spec.rb[1:11:2] | passed | 0.07322 seconds |
|
|
40
|
+
./spec/historiographer_spec.rb[1:11:3] | passed | 0.07853 seconds |
|
|
41
|
+
./spec/historiographer_spec.rb[1:11:4] | passed | 0.05709 seconds |
|
|
42
|
+
./spec/historiographer_spec.rb[1:12:1] | passed | 0.07377 seconds |
|
|
43
|
+
./spec/historiographer_spec.rb[1:12:2] | passed | 0.05676 seconds |
|
|
44
|
+
./spec/historiographer_spec.rb[1:12:3] | passed | 0.07933 seconds |
|
|
45
|
+
./spec/historiographer_spec.rb[1:12:4] | passed | 0.07886 seconds |
|
|
46
|
+
./spec/historiographer_spec.rb[1:12:5] | passed | 0.07857 seconds |
|
|
47
|
+
./spec/historiographer_spec.rb[1:12:6] | passed | 0.06854 seconds |
|
|
48
|
+
./spec/historiographer_spec.rb[1:12:7] | passed | 0.08045 seconds |
|
|
49
|
+
./spec/historiographer_spec.rb[1:13:1] | passed | 0.07652 seconds |
|
|
50
|
+
./spec/historiographer_spec.rb[1:14:1] | passed | 0.05116 seconds |
|
|
51
|
+
./spec/historiographer_spec.rb[1:14:2] | passed | 0.06274 seconds |
|
|
52
|
+
./spec/historiographer_spec.rb[1:14:3] | passed | 0.0698 seconds |
|
|
53
|
+
./spec/historiographer_spec.rb[1:14:4] | passed | 0.05648 seconds |
|
|
54
|
+
./spec/historiographer_spec.rb[1:15:1] | passed | 0.05722 seconds |
|
|
55
|
+
./spec/historiographer_spec.rb[1:15:2] | passed | 0.08476 seconds |
|
|
56
|
+
./spec/historiographer_spec.rb[1:16:1] | passed | 0.04781 seconds |
|
|
57
|
+
./spec/historiographer_spec.rb[1:16:2] | passed | 0.04802 seconds |
|
|
58
|
+
./spec/historiographer_spec.rb[1:16:3] | passed | 0.06019 seconds |
|
|
59
|
+
./spec/historiographer_spec.rb[1:16:4] | passed | 0.07289 seconds |
|
|
60
|
+
./spec/historiographer_spec.rb[1:17:1:1] | passed | 0.07235 seconds |
|
|
61
|
+
./spec/historiographer_spec.rb[1:17:1:2] | passed | 0.06784 seconds |
|
|
62
|
+
./spec/historiographer_spec.rb[1:17:2:1] | passed | 0.05847 seconds |
|
|
62
63
|
./spec/integration/historiographer_safe_integration_spec.rb[1:1:1] | passed | 0.06664 seconds |
|
|
63
64
|
./spec/integration/historiographer_safe_integration_spec.rb[1:1:2] | passed | 0.07357 seconds |
|
|
64
65
|
./spec/integration/historiographer_safe_integration_spec.rb[1:1:3] | passed | 0.05953 seconds |
|
|
@@ -823,6 +823,36 @@ describe Historiographer do
|
|
|
823
823
|
expect(post.latest_snapshot.comment_count).to eq 1
|
|
824
824
|
end
|
|
825
825
|
|
|
826
|
+
it "snapshots all children when one has a null_snapshot history record" do
|
|
827
|
+
# This tests a bug where if one child record already has a history with snapshot_id: nil,
|
|
828
|
+
# the snapshot would silently fail to update it due to belongs_to :user validation
|
|
829
|
+
Historiographer::Configuration.mode = :snapshot_only
|
|
830
|
+
|
|
831
|
+
# Create records without triggering histories (snapshot_only mode)
|
|
832
|
+
author1 = Author.create(full_name: 'Author One', history_user_id: user.id)
|
|
833
|
+
author2 = Author.create(full_name: 'Author Two', history_user_id: user.id)
|
|
834
|
+
|
|
835
|
+
# Manually create a null_snapshot history for author1 (simulating previous state)
|
|
836
|
+
# This mimics what happens when a record has a history created outside of snapshot
|
|
837
|
+
AuthorHistory.create!(
|
|
838
|
+
author_id: author1.id,
|
|
839
|
+
full_name: author1.full_name,
|
|
840
|
+
history_user_id: nil, # No user - this will trigger the bug
|
|
841
|
+
snapshot_id: nil, # Null snapshot
|
|
842
|
+
history_started_at: Time.current
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
expect(AuthorHistory.count).to eq 1
|
|
846
|
+
expect(AuthorHistory.where(snapshot_id: nil).count).to eq 1
|
|
847
|
+
|
|
848
|
+
# Now snapshot author1 - this should work even though there's a null_snapshot
|
|
849
|
+
# The bug: without_history_user_id doesn't prevent belongs_to :user validation
|
|
850
|
+
author1.snapshot
|
|
851
|
+
|
|
852
|
+
expect(AuthorHistory.where.not(snapshot_id: nil).count).to eq 1
|
|
853
|
+
expect(AuthorHistory.first.snapshot_id).to be_present
|
|
854
|
+
end
|
|
855
|
+
|
|
826
856
|
it "doesn't explode" do
|
|
827
857
|
project = Project.create(name: "test_project")
|
|
828
858
|
project_file = ProjectFile.create(project: project, name: "test_file", content: "Hello world")
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: historiographer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.4.
|
|
4
|
+
version: 4.4.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- brettshollenberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-01-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|