diff --git a/CHANGELOG.md b/CHANGELOG.md index 14bb4e9..e5f4842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ CHANGELOG - **Unreleased** * [View Diff](https://github.com/westonganger/active_snapshot/compare/v0.5.0...master) - * Nothing yet + * [#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. - **v0.5.0** - Nov 8, 2024 * [View Diff](https://github.com/westonganger/active_snapshot/compare/v0.4.0...v0.5.0) diff --git a/README.md b/README.md index b18b342..f883bbf 100644 --- a/README.md +++ b/README.md @@ -131,9 +131,11 @@ end Now when you run `create_snapshot!` the associations will be tracked accordingly -# Reifying Snapshot Items +# Reifying Snapshots -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. +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. + +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. ```ruby reified_parent, reified_children_hash = snapshot.fetch_reified_items @@ -149,6 +151,32 @@ reified_parent, reified_children_hash = snapshot.fetch_reified_items reified_children_hash.first.instance_variable_set("@readonly", false) ``` +# Important Data Considerations / Warnings + +### Dropping columns + +If you plan to use the snapshot restore capabilities please be aware: + +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. + +I recommend that you add an entry to this in your applications safe-migrations guidelines. + +If you would like to detect if this situation has already ocurred you can use the following script: + +```ruby +SnapshotItem.all.each do |snapshot_item| + snapshot_item.object.keys.each do |key| + klass = Class.const_get(snapshot_item.item_type) + + if !klass.column_names.include?(key) + invalid_data = snapshot_item.object.slice(*klass.column_names) + + raise "invalid data found - #{invalid_data}" + end + end +end +``` + # Key Models Provided & Additional Customizations 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. diff --git a/lib/active_snapshot/models/snapshot.rb b/lib/active_snapshot/models/snapshot.rb index 8f77fb7..3f515db 100644 --- a/lib/active_snapshot/models/snapshot.rb +++ b/lib/active_snapshot/models/snapshot.rb @@ -109,7 +109,15 @@ def fetch_reified_items(readonly: true) reified_parent = nil snapshot_items.each do |si| - reified_item = si.item_type.constantize.new(si.object) + reified_item = si.item_type.constantize.new + + si.object.each do |k,v| + if reified_item.respond_to?("#{k}=") + reified_item[k] = v + else + # database column was likely dropped since the snapshot was created + end + end if readonly reified_item.readonly! diff --git a/lib/active_snapshot/models/snapshot_item.rb b/lib/active_snapshot/models/snapshot_item.rb index 36e86b1..96201b3 100644 --- a/lib/active_snapshot/models/snapshot_item.rb +++ b/lib/active_snapshot/models/snapshot_item.rb @@ -51,7 +51,13 @@ def restore_item! self.item = item_klass.new end - item.assign_attributes(object) + object.each do |k,v| + if item.respond_to?("#{k}=") + item[k] = v + else + # database column was likely dropped since the snapshot was created + end + end item.save!(validate: false, touch: false) end diff --git a/test/models/snapshot_item_test.rb b/test/models/snapshot_item_test.rb index 407badf..2f1bd70 100644 --- a/test/models/snapshot_item_test.rb +++ b/test/models/snapshot_item_test.rb @@ -67,4 +67,17 @@ def test_restore_item! @snapshot_item.restore_item! end + def test_restore_item_handles_dropped_columns! + snapshot = @snapshot_klass.includes(:snapshot_items).first + + snapshot_item = snapshot.snapshot_items.first + + attrs = snapshot_item.object + attrs["foo"] = "bar" + + snapshot_item.update!(object: attrs) + + snapshot_item.restore_item! + end + end diff --git a/test/models/snapshot_test.rb b/test/models/snapshot_test.rb index d19eff8..14094eb 100644 --- a/test/models/snapshot_test.rb +++ b/test/models/snapshot_test.rb @@ -149,6 +149,19 @@ def test_fetch_reified_items_with_sti_class assert_equal comment_content, reified_items.second[:comments].first.content end + def test_fetch_reified_items_handles_dropped_columns! + snapshot = @snapshot_klass.first + + snapshot_item = snapshot.snapshot_items.first + + attrs = snapshot_item.object + attrs["foo"] = "bar" + + snapshot_item.update!(object: attrs) + + reified_items = snapshot.fetch_reified_items(readonly: false) + end + def test_single_model_snapshots_without_children instance = ParentWithoutChildren.create!({a: 1, b: 2})