reversable_data_migration 0.1.0

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.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ db
2
+ .test.db
3
+ gem*
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,43 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ reversable_data_migration (0.1.0)
5
+ activerecord
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ activemodel (3.2.1)
11
+ activesupport (= 3.2.1)
12
+ builder (~> 3.0.0)
13
+ activerecord (3.2.1)
14
+ activemodel (= 3.2.1)
15
+ activesupport (= 3.2.1)
16
+ arel (~> 3.0.0)
17
+ tzinfo (~> 0.3.29)
18
+ activesupport (3.2.1)
19
+ i18n (~> 0.6)
20
+ multi_json (~> 1.0)
21
+ arel (3.0.0)
22
+ builder (3.0.0)
23
+ diff-lcs (1.1.3)
24
+ i18n (0.6.0)
25
+ multi_json (1.0.4)
26
+ rspec (2.8.0)
27
+ rspec-core (~> 2.8.0)
28
+ rspec-expectations (~> 2.8.0)
29
+ rspec-mocks (~> 2.8.0)
30
+ rspec-core (2.8.0)
31
+ rspec-expectations (2.8.0)
32
+ diff-lcs (~> 1.1.2)
33
+ rspec-mocks (2.8.0)
34
+ sqlite3 (1.3.5)
35
+ tzinfo (0.3.31)
36
+
37
+ PLATFORMS
38
+ ruby
39
+
40
+ DEPENDENCIES
41
+ reversable_data_migration!
42
+ rspec
43
+ sqlite3
data/Manifest ADDED
@@ -0,0 +1,4 @@
1
+ README.rdoc
2
+ Rakefile
3
+ lib/reversable_data_migration.rb
4
+ Manifest
data/README.rdoc ADDED
@@ -0,0 +1,21 @@
1
+ = Reversable Data Migration
2
+
3
+ Need to update a small amount of data in migration? But still want to make it reversable? Reversable Data Migration comes to the rescue.
4
+
5
+ == Example usage
6
+
7
+ class RemoveStateFromProduct < ActiveRecord::Migration
8
+ def self.up
9
+ backup_data = []
10
+ Product.all.each do |product|
11
+ backup_data << {:id => product.id, :state => product.state}
12
+ end
13
+ backup backup_data
14
+ remove_column :products, :state
15
+ end
16
+
17
+ def self.down
18
+ add_column :products, :state, :string
19
+ restore Permission
20
+ end
21
+ end
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,94 @@
1
+ module ReversableDataMigration
2
+
3
+ def location_backup_files
4
+ "#{(Rails.version =~ /^2/) ? RAILS_ROOT : Rails.root.to_s}/db/migrate/backup_data"
5
+ end
6
+
7
+ def default_backupfile
8
+ "#{location_backup_files}/#{name.underscore}.yml" # name.underscore => name of migration
9
+ end
10
+
11
+ def full_path_of file
12
+ "#{location_backup_files}/#{file}.yml"
13
+ end
14
+
15
+ def default_or_specific_file file
16
+ if file
17
+ full_path_of file
18
+ else
19
+ default_backupfile
20
+ end
21
+ end
22
+
23
+ def backup data, file=nil
24
+ unless File.directory?(location_backup_files)
25
+ FileUtils.mkdir_p(location_backup_files)
26
+ end
27
+ file = default_or_specific_file(file)
28
+ puts "-- writing backup data (#{data.count} records) to #{file}"
29
+ File.open( file , 'w' ) do |out|
30
+ YAML.dump( data, out )
31
+ end
32
+ end
33
+
34
+ def destroy_created_records klass, file=nil
35
+ file = default_or_specific_file(file)
36
+ test_record = first_record(file)
37
+ process_records(klass, file){ |object, object_hash| object.destroy }
38
+ raise "Destroying objects failed" if test_record.blank? || test_record[:id].blank? || klass.find_by_id(test_record[:id])
39
+ end
40
+
41
+ def restore klass, file=nil
42
+ file = default_or_specific_file(file)
43
+ test_record = first_record file
44
+ puts "-- restore data from #{file}"
45
+ process_records(klass,file) do |object, object_hash|
46
+ object_hash.select{|k,v| k != :id}.each do |key, value|
47
+ object.send("#{key}=", value)
48
+ end
49
+ object.save
50
+ end
51
+ end
52
+
53
+ def restore_batch
54
+ @transaction = true
55
+ @to_delete_after_transaction = []
56
+ yield
57
+ @to_delete_after_transaction.each do |file|
58
+ delete_file(file)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def process_records klass, file
65
+ count = 0
66
+ File.open( file ) { |yf| YAML::load( yf ) }.each do |object_hash|
67
+ object = klass.find object_hash[:id]
68
+ yield object, object_hash
69
+ count += 1
70
+ end
71
+ puts "-- processed #{count} records"
72
+ unless @transaction
73
+ delete_file(file)
74
+ else
75
+ @to_delete_after_transaction << file
76
+ end
77
+ end
78
+
79
+ def delete_file file
80
+ puts "-- deleting backupfile #{file}"
81
+ File.delete file
82
+ end
83
+
84
+ def first_record file
85
+ File.open( file ) { |yf| YAML::load( yf ) }.first
86
+ end
87
+
88
+ end
89
+
90
+ if Rails.version =~ /^2/
91
+ ActiveRecord::Migration.send(:extend, ReversableDataMigration)
92
+ else
93
+ ActiveRecord::Migration.send(:include, ReversableDataMigration)
94
+ end
@@ -0,0 +1,153 @@
1
+ require 'spec_helper'
2
+ module ReversableDataMigration
3
+ RAILS_ROOT = File.dirname(__FILE__)
4
+ end
5
+
6
+ describe ReversableDataMigration do
7
+
8
+ before :each do
9
+ Product.delete_all
10
+ `rm -rf #{File.dirname(__FILE__)}/db`
11
+ Product.create!(:state => "available")
12
+ Product.create!(:state => "not_available")
13
+ end
14
+
15
+ it "should write backup data to a yml file with the name of the migration" do
16
+
17
+ class RemoveStateFromProduct < ActiveRecord::Migration
18
+ def self.up
19
+ backup_data = []
20
+ Product.all.each do |product|
21
+ backup_data << {:id => product.id, :state => product.state}
22
+ end
23
+ backup backup_data
24
+ end
25
+ end
26
+
27
+ RemoveStateFromProduct.up
28
+
29
+ yaml = File.open( File.dirname(__FILE__) + "/db/migrate/backup_data/remove_state_from_product.yml" ) { |yf| YAML::load( yf ) }
30
+
31
+ yaml.should == [
32
+ {:id => 1, :state => "available"},
33
+ {:id => 2, :state => "not_available"}
34
+ ]
35
+ end
36
+
37
+ it "should write backup data to a yml file with the name given by the user" do
38
+
39
+ class RemoveStateFromProduct < ActiveRecord::Migration
40
+ def self.up
41
+ backup_data = []
42
+ Product.all.each do |product|
43
+ backup_data << {:id => product.id, :state => product.state}
44
+ end
45
+ backup backup_data, "some_name"
46
+ end
47
+ end
48
+
49
+ RemoveStateFromProduct.up
50
+
51
+ yaml = File.open( File.dirname(__FILE__) + "/db/migrate/backup_data/some_name.yml" ) { |yf| YAML::load( yf ) }
52
+
53
+ yaml.should == [
54
+ {:id => 3, :state => "available"},
55
+ {:id => 4, :state => "not_available"}
56
+ ]
57
+ end
58
+
59
+ it "should rollback properly" do
60
+
61
+ class RemoveStateFromProduct < ActiveRecord::Migration
62
+ def self.up
63
+ backup_data = []
64
+ Product.all.each do |product|
65
+ backup_data << {:id => product.id, :state => product.state}
66
+ end
67
+ backup backup_data
68
+ remove_column :products, :state
69
+ end
70
+
71
+ def self.down
72
+ add_column :products, :state, :string
73
+ restore Product
74
+ end
75
+ end
76
+
77
+ RemoveStateFromProduct.up
78
+ RemoveStateFromProduct.down
79
+ end
80
+
81
+ it "should delete created records" do
82
+
83
+ class RemoveStateFromProduct < ActiveRecord::Migration
84
+ def self.up
85
+ backup [{:id => Product.create.id}]
86
+ end
87
+
88
+ def self.down
89
+ destroy_created_records Product
90
+ end
91
+ end
92
+
93
+ RemoveStateFromProduct.up
94
+ Product.count.should == 3
95
+ RemoveStateFromProduct.down
96
+ Product.count.should == 2
97
+ end
98
+
99
+
100
+ it "should remove backup files after everything is recovered" do
101
+ class RemoveStateFromProduct < ActiveRecord::Migration
102
+ def self.up
103
+ product = Product.first
104
+ backup [{:id => product.id, :state => product.state}]
105
+ product.update_attribute("state", "something")
106
+ backup [{:id => Product.create.id}], "second_backup"
107
+ end
108
+
109
+ def self.down
110
+ restore_batch do
111
+ restore Product
112
+ destroy_created_records Product, "second_backup"
113
+ end
114
+ end
115
+ end
116
+
117
+ RemoveStateFromProduct.up
118
+ RemoveStateFromProduct.down
119
+
120
+ backup_files.should == []
121
+ end
122
+
123
+
124
+ it "should keep backup files if an error occurs" do
125
+ class RemoveStateFromProduct < ActiveRecord::Migration
126
+ def self.up
127
+ product = Product.first
128
+ backup [{:id => product.id, :state => product.state}]
129
+ product.update_attribute("state", "something")
130
+ backup [{:id => Product.create.id}], "second_backup"
131
+ end
132
+
133
+ def self.down
134
+ restore_batch do
135
+ restore Product
136
+ raise "error"
137
+ destroy_created_records Product, "second_backup"
138
+ end
139
+ end
140
+ end
141
+
142
+ RemoveStateFromProduct.up
143
+ RemoveStateFromProduct.down rescue nil
144
+
145
+ backup_files.should == ["remove_state_from_product.yml", "second_backup.yml"]
146
+ end
147
+
148
+ private
149
+
150
+ def backup_files
151
+ `ls #{File.join(File.dirname(__FILE__), 'db/migrate/backup_data')}`.split("\n")
152
+ end
153
+ end
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'sqlite3'
4
+ require 'active_record'
5
+ class Rails
6
+ def self.version
7
+ "2"
8
+ end
9
+ end
10
+ require 'reversable_data_migration'
11
+
12
+ RSpec.configure do |config|
13
+
14
+ end
15
+
16
+
17
+
18
+ # connect to database. This will create one if it doesn't exist
19
+ MY_DB_NAME = ".test.db"
20
+ `rm #{MY_DB_NAME}`
21
+ MY_DB = SQLite3::Database.new(MY_DB_NAME)
22
+
23
+ # get active record set up
24
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => MY_DB_NAME)
25
+
26
+ # create your AR class
27
+ class Product < ActiveRecord::Base
28
+
29
+ end
30
+
31
+ # do a quick pseudo migration. This should only get executed on the first run
32
+ if !Product.table_exists?
33
+ ActiveRecord::Base.connection.create_table(:products) do |t|
34
+ t.column :state, :string
35
+ end
36
+ end
37
+
38
+ $stdout = File.new('/dev/null', 'w')
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: reversable_data_migration
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Tom Maeckelberghe
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-02-12 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: sqlite3
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id002
48
+ description: Backup data is saved to yaml files. The reverse process load the yaml and restores the records. Works for new and updates not for deletions.
49
+ email:
50
+ - tom.maeckelberghe@gmail.com
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files: []
56
+
57
+ files:
58
+ - .gitignore
59
+ - Gemfile
60
+ - Gemfile.lock
61
+ - Manifest
62
+ - README.rdoc
63
+ - Rakefile
64
+ - VERSION
65
+ - lib/reversable_data_migration.rb
66
+ - spec/backup_spec.rb
67
+ - spec/spec_helper.rb
68
+ homepage: https://github.com/tomkurt/Reversable-Data-Migration
69
+ licenses: []
70
+
71
+ post_install_message:
72
+ rdoc_options: []
73
+
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ hash: 3
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ hash: 3
91
+ segments:
92
+ - 0
93
+ version: "0"
94
+ requirements: []
95
+
96
+ rubyforge_project: reversable_migration_helper
97
+ rubygems_version: 1.8.10
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: Makes activerecord data migrations reversable.
101
+ test_files: []
102
+