reversable_data_migration 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+