Skip to content

Commit b0b6871

Browse files
authored
On restore bypass assignment for snapshot object data where the associated column no longer exists (#66)
Ensure `SnapshotItem#restore_item!` and `Snapshot#fetch_reified_items` bypass assignment for snapshot object data where the associated column no longer exists. Solves #58
1 parent 16305bb commit b0b6871

File tree

6 files changed

+73
-4
lines changed

6 files changed

+73
-4
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ CHANGELOG
33

44
- **Unreleased**
55
* [View Diff](https://github.com/westonganger/active_snapshot/compare/v0.5.0...master)
6+
* [#66](https://github.com/westonganger/active_snapshot/pull/66) - Ensure `SnapshotItem#restore_item!` and `Snapshot#fetch_reified_items` bypass assignment for snapshot object data where the associated column no longer exists.
67
* [#63](https://github.com/westonganger/active_snapshot/pull/63) - Fix bug when enum value is nil
78

89
- **v0.5.0** - Nov 8, 2024

README.md

+30-2
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,11 @@ end
131131

132132
Now when you run `create_snapshot!` the associations will be tracked accordingly
133133

134-
# Reifying Snapshot Items
134+
# Reifying Snapshots
135135

136-
You can view all of the reified snapshot items by calling the following method. Its completely up to you on how to use this data.
136+
A reified record refers to an ActiveRecord instance where the local objects data is set to match the snaphotted data, but the database remains changed.
137+
138+
You can view all of the "reified" snapshot items by calling the following method. Its completely up to you on how to use this data.
137139

138140
```ruby
139141
reified_parent, reified_children_hash = snapshot.fetch_reified_items
@@ -166,6 +168,32 @@ attrs_not_changed = old_attrs.to_a.intersection(new_attrs.to_a).to_h
166168
attrs_changed = new_attrs.to_a - attrs_not_changed.to_a
167169
```
168170

171+
# Important Data Considerations / Warnings
172+
173+
### Dropping columns
174+
175+
If you plan to use the snapshot restore capabilities please be aware:
176+
177+
Whenever you drop a database column and there already exists snapshots of that model then you are kind of silently breaking your restore mechanism. Because now the application will not be able to assign data to columns that dont exist on the model. We work around this by bypassing the attribute assignment for snapshot item object entries that does not correlate to a current database column.
178+
179+
I recommend that you add an entry to this in your applications safe-migrations guidelines.
180+
181+
If you would like to detect if this situation has already ocurred you can use the following script:
182+
183+
```ruby
184+
SnapshotItem.all.each do |snapshot_item|
185+
snapshot_item.object.keys.each do |key|
186+
klass = Class.const_get(snapshot_item.item_type)
187+
188+
if !klass.column_names.include?(key)
189+
invalid_data = snapshot_item.object.slice(*klass.column_names)
190+
191+
raise "invalid data found - #{invalid_data}"
192+
end
193+
end
194+
end
195+
```
196+
169197
# Key Models Provided & Additional Customizations
170198

171199
A key aspect of this library is its simplicity and small API. For major functionality customizations we encourage you to first delete this gem and then copy this gems code directly into your repository.

lib/active_snapshot/models/snapshot.rb

+9-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,15 @@ def fetch_reified_items(readonly: true)
112112
reified_parent = nil
113113

114114
snapshot_items.each do |si|
115-
reified_item = si.item_type.constantize.new(si.object)
115+
reified_item = si.item_type.constantize.new
116+
117+
si.object.each do |k,v|
118+
if reified_item.respond_to?("#{k}=")
119+
reified_item[k] = v
120+
else
121+
# database column was likely dropped since the snapshot was created
122+
end
123+
end
116124

117125
if readonly
118126
reified_item.readonly!

lib/active_snapshot/models/snapshot_item.rb

+7-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,13 @@ def restore_item!
5151
self.item = item_klass.new
5252
end
5353

54-
item.assign_attributes(object)
54+
object.each do |k,v|
55+
if item.respond_to?("#{k}=")
56+
item[k] = v
57+
else
58+
# database column was likely dropped since the snapshot was created
59+
end
60+
end
5561

5662
item.save!(validate: false, touch: false)
5763
end

test/models/snapshot_item_test.rb

+13
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,17 @@ def test_restore_item!
6767
@snapshot_item.restore_item!
6868
end
6969

70+
def test_restore_item_handles_dropped_columns!
71+
snapshot = @snapshot_klass.includes(:snapshot_items).first
72+
73+
snapshot_item = snapshot.snapshot_items.first
74+
75+
attrs = snapshot_item.object
76+
attrs["foo"] = "bar"
77+
78+
snapshot_item.update!(object: attrs)
79+
80+
snapshot_item.restore_item!
81+
end
82+
7083
end

test/models/snapshot_test.rb

+13
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,19 @@ def test_fetch_reified_items_with_sti_class
192192
assert_equal comment_content, reified_items.second[:comments].first.content
193193
end
194194

195+
def test_fetch_reified_items_handles_dropped_columns!
196+
snapshot = @snapshot_klass.first
197+
198+
snapshot_item = snapshot.snapshot_items.first
199+
200+
attrs = snapshot_item.object
201+
attrs["foo"] = "bar"
202+
203+
snapshot_item.update!(object: attrs)
204+
205+
reified_items = snapshot.fetch_reified_items(readonly: false)
206+
end
207+
195208
def test_single_model_snapshots_without_children
196209
instance = ParentWithoutChildren.create!({a: 1, b: 2})
197210

0 commit comments

Comments
 (0)