Permalink
Browse files

Add a warning when columns are deleted with update_columns.

The warning can be disabled. Reformat and spellcheck docs a little more.
  • Loading branch information...
ajh committed May 20, 2008
1 parent 2171bff commit f521949514865d75d908f3bec6943ec1435e6604
Showing with 138 additions and 31 deletions.
  1. +31 −7 README
  2. +47 −17 lib/acts_as_soft_deletable.rb
  3. +15 −0 test/fixtures/migrations/5_remove_column.rb
  4. +45 −7 test/test_migration.rb
View
38 README
@@ -25,11 +25,31 @@ They can later be restored easily.
== Compare to acts_as_paranoid
-Acts_as_paranoid takes the approach of using a deleted_at flag in the models table. If the deleted_at column has a value, the row is considered 'deleted'. The problem with this approach is that all finds on the model have to exclude 'deleted' rows. This turns out be be a challenge. Acts_as_paranoid patches the ActiveRecord internals to accomplish this, but it is fragile and could break with future changes to ActiveRecord. Also, some of the more exotic finds currently don't work (has_many :through with polymorphism as of March 2008) and supporting them means running on an upgrade treadmill to keep up with the evolution of ActiveRecord.
-
-This plugin avoids these problems by allowing the row to be deleted and archiving it into another table. The behavior of ActiveRecord::Base#Find doesn't have to change which should mean that this plugin is more immune to breaking due to ActiveRecord development. Queries on the live table will also be faster in the case of lots of deleted rows, because they will be in a seperate table. The tradeoff is that the deleted table needs to be maintained along with the live table. For example, if the artists table adds a column in a future migration, then the deleted_artists table needs that column as well.
-
-A migration helper is available (see below) that will help keep the deleted table in sync. Also, a unit test helper is provided (again, see below) which adds unit tests to the model ensuring soft delete is working. If this is used, the test will fail if the deleted table gets out of sync.
+Acts_as_paranoid takes the approach of using a deleted_at flag in the models
+table. If the deleted_at column has a value, the row is considered 'deleted'.
+The problem with this approach is that all finds on the model have to exclude
+'deleted' rows. This turns out be be a challenge. Acts_as_paranoid patches the
+ActiveRecord internals to accomplish this, but it is fragile and could break
+with future changes to ActiveRecord. Also, some of the more exotic finds
+currently don't work (has_many :through with polymorphism as of March 2008) and
+supporting them means running on an upgrade treadmill to keep up with the
+evolution of ActiveRecord.
+
+This plugin avoids these problems by allowing the row to be deleted and
+archiving it into another table. The behavior of ActiveRecord::Base#Find
+doesn't have to change which should mean that this plugin is more immune to
+breaking due to ActiveRecord development. Queries on the live table will also
+be faster in the case of lots of deleted rows, because they will be in a
+separate table.
+
+The tradeoff is that the deleted table needs to be maintained along with the
+live table. For example, if the artists table adds a column in a future
+migration, then the deleted_artists table needs that column as well.
+
+A migration helper is available (see below) that will help keep the deleted
+table in sync. Also, a unit test helper is provided (again, see below) which
+adds unit tests to the model ensuring soft delete is working. If this is used,
+the test will fail if the deleted table gets out of sync.
== Setup
@@ -56,7 +76,8 @@ and setup the deleted table with the following migration:
end
end
-Any changes to the original table (such as adding a column) should be reflected in the deleted table. Use the update_columns method:
+Any changes to the original table (such as adding a column) should be reflected
+in the deleted table. Use the update_columns method:
class AddSkuColumn < ActiveRecord::Migration
def self.up
@@ -70,7 +91,10 @@ Any changes to the original table (such as adding a column) should be reflected
end
end
-Note that update_columns will happily delete columns if asked which will lose data forever. I'm not sure if this behavior is fine or horrifying, but it's definitely something to keep in mind. I'd appreciate feedback on this.
+Note that update_columns will happily delete columns if asked. In this case a
+warning will be issued when the migration is run alerting the developer to the
+situation. This warning can be disabled, see:
+ActiveRecord::Acts::SoftDeletable#remove_column_warning_enabled=
=== Unit tests
@@ -1,14 +1,26 @@
module ActiveRecord #:nodoc:
module Acts #:nodoc:
- module SoftDeletable #:nodoc:
+ # See the README file for general usage, or ClassMethods#acts_as_soft_deletable for more info.
+ module SoftDeletable
+ @remove_column_warning_enabled = true
+
+ # Returns whether the remove column warning is enabled
+ def self.remove_column_warning_enabled?; @remove_column_warning_enabled end
+
+ # Sets whether the remove column warning is enabled
+ def self.remove_column_warning_enabled=(boolean); @remove_column_warning_enabled = (boolean ? true : false) end
+
module ClassMethods
- # Specify this act if you wish to archive deleted rows in a special deleted table so that
- # they can be later restored.
+ # Specify this act if you wish to archive deleted rows in a special
+ # deleted table so that they can be later restored.
#
- # This includes and extends Live::InstanceMethods and Live::ClassMethods into this class.
+ # This includes and extends Live::InstanceMethods and
+ # Live::ClassMethods into this class.
#
- # It will also create a new ActiveRecord::Base class named after this class with the suffix <tt>::Deleted</tt> added.
- # The new class is used for dealing with rows that have been deleted. See the README for more info and examples.
+ # It will also create a new ActiveRecord::Base class named after this
+ # class with the suffix <tt>::Deleted</tt> added. The new class is
+ # used for dealing with rows that have been deleted. See the README for
+ # more info and examples.
def acts_as_soft_deletable
# don't allow multiple calls
return if self.included_modules.include?(Live::InstanceMethods)
@@ -32,9 +44,11 @@ def acts_as_soft_deletable
module Deleted #:nodoc:
- # These methods will be available as class methods on the deleted class.
+ # These methods will be available as class methods on the deleted
+ # class.
module ClassMethods
- # Creates a deleted table by introspecting on the live table. Useful in a migration #up method.
+ # Creates a deleted table by introspecting on the live table. Useful
+ # in a migration #up method.
def create_table(create_table_options = {})
connection.create_table(table_name, create_table_options) do |t|
live_class.columns.select{|col| col.name != live_class.primary_key}.each do |col|
@@ -49,8 +63,15 @@ def drop_table(drop_table_options = {})
connection.drop_table(table_name, drop_table_options)
end
- # Updates the deleted table by adding or removing rows to match the live table.
- # This is useful to call after adding or deleting columns in the live table.
+ # Updates the deleted table by adding or removing rows to match the
+ # live table. This is useful to call after adding or deleting
+ # columns in the live table.
+ #
+ # A warning will be printed if a column is being removed just to make
+ # sure the behavior is expected. The warning can be turned off by
+ # setting
+ # ActiveRecord::Acts::SoftDeletable#remove_column_warning_enabled= to
+ # false.
def update_columns
live_specs = returning({}) do |h|
live_class.columns.each do |col|
@@ -74,6 +95,9 @@ def update_columns
end
(deleted_specs.keys - live_specs.keys).each do |name|
+ if ActiveRecord::Acts::SoftDeletable.remove_column_warning_enabled?
+ warn "Acts_as_soft_deletable is removing column #{table_name}.#{name}. You can disable this warning by setting blah = false in your migration."
+ end
connection.remove_column table_name, name
end
@@ -82,9 +106,12 @@ def update_columns
end
end
- # These methods will be available as instance methods on the deleted class.
+ # These methods will be available as instance methods on the deleted
+ # class.
module InstanceMethods
- # Restore the model from deleted status. Will destroy the deleted record and recreate the live record. This is done in a transaction and will rollback if problems occur.
+ # Restore the model from deleted status. Will destroy the deleted
+ # record and recreate the live record. This is done in a transaction
+ # and will rollback if problems occur.
def undestroy!
self.class.transaction do
model = self.class.live_class.new
@@ -101,15 +128,17 @@ def undestroy!
module Live #:nodoc:
- # These methods will be available as class methods for the Model class that invoked acts_as_soft_deletable
+ # These methods will be available as class methods for the Model class
+ # that invoked acts_as_soft_deletable
module ClassMethods
# Returns Class object of deleted class
def deleted_class
@deleted_class
end
end
- # These methods will be available as instance methods for the Model class that invoked acts_as_soft_deletable
+ # These methods will be available as instance methods for the Model
+ # class that invoked acts_as_soft_deletable
module InstanceMethods
def self.included(base)
base.class_eval do
@@ -118,9 +147,10 @@ def self.included(base)
end
end
- # Wraps ActiveRecord::Base#destroy to provide the soft deleting behavoir.
- # The insert into the deleted table is protected with a transaciton and
- # will be rolled back if destroy raises any exception.
+ # Wraps ActiveRecord::Base#destroy to provide the soft deleting
+ # behavior. The insert into the deleted table is protected with a
+ # transaction and will be rolled back if destroy raises any
+ # exception.
def destroy_with_soft_delete
self.class.transaction do
deleted = self.class.deleted_class.new
@@ -0,0 +1,15 @@
+class RemoveColumn < ActiveRecord::Migration
+ class Thing < ActiveRecord::Base
+ acts_as_soft_deletable
+ end
+
+ def self.up
+ remove_column :things, :sku
+ Thing::Deleted.update_columns
+ end
+
+ def self.down
+ add_column :things, :sku, :string
+ Thing::Deleted.update_columns
+ end
+end
View
@@ -7,7 +7,8 @@ class Thing < ActiveRecord::Base
class TestMigration < SoftDeleteTestCase
- def test_should_create_deleted_table
+ # a santiy check
+ def test_that_i_can_do_a_migration_from_this_test_case
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
migrate_up(3)
@@ -21,18 +22,42 @@ def test_should_create_deleted_table
assert_raises(ActiveRecord::StatementInvalid) { Thing::Deleted.find_by_title('blah blah') }
end
+ def test_should_create_deleted_table
+ migrate_up(2)
+ assert_raises(ActiveRecord::StatementInvalid) { Thing::Deleted.find_by_title('blah blah') }
+
+ migrate_up(3)
+ t = Thing.create! :title => 'blah blah', :price => 123.45, :type => 'Thing'
+ assert_model_soft_deletes(t)
+ end
+
def test_should_help_when_adding_columns
- assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
+ migrate_up(4)
+ t = Thing.create! :title => 'blah blah', :price => 123.45, :type => 'Thing', :sku => 'XYZ123abc'
+ assert_model_soft_deletes(t)
+ end
+ def test_should_help_when_removing_columns_and_warn
migrate_up(4)
- t = Thing.create! :title => 'blah blah', :price => 123.45, :type => 'Thing'
- assert_model_soft_deletes(t)
+ stderr = run_and_capture_stderr do
+ migrate_up(5)
+ end
- migrate_down
+ assert_match %r/removing column deleted_things.sku/, stderr
+ end
- assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
- assert_raises(ActiveRecord::StatementInvalid) { Thing::Deleted.find_by_title('blah blah') }
+ def test_should_be_able_to_supress_warning_when_removing_columns
+ migrate_up(4)
+
+ stderr = run_and_capture_stderr do
+ ActiveRecord::Acts::SoftDeletable.remove_column_warning_enabled = false
+ migrate_up(5)
+ end
+
+ assert_equal "", stderr
+ ensure
+ ActiveRecord::Acts::SoftDeletable.remove_column_warning_enabled = true
end
def teardown
@@ -76,5 +101,18 @@ def assert_model_soft_deletes(model)
assert_raises(ActiveRecord::RecordNotFound) { deleted_klass.find model.id }
end
+ def run_and_capture_stderr
+ io = StringIO.new '', 'w+'
+ $stderr = io
+
+ yield
+
+ io.rewind
+ return io.read
+ ensure
+ $stderr = STDERR
+ end
end
end
+
+

0 comments on commit f521949

Please sign in to comment.