reversible_data_migration 0.1.1

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,30 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ reversible_data_migration (0.1.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ activerecord (2.3.14)
10
+ activesupport (= 2.3.14)
11
+ activesupport (2.3.14)
12
+ diff-lcs (1.1.3)
13
+ rspec (2.8.0)
14
+ rspec-core (~> 2.8.0)
15
+ rspec-expectations (~> 2.8.0)
16
+ rspec-mocks (~> 2.8.0)
17
+ rspec-core (2.8.0)
18
+ rspec-expectations (2.8.0)
19
+ diff-lcs (~> 1.1.2)
20
+ rspec-mocks (2.8.0)
21
+ sqlite3 (1.3.5)
22
+
23
+ PLATFORMS
24
+ ruby
25
+
26
+ DEPENDENCIES
27
+ activerecord (= 2.3.14)
28
+ reversible_data_migration!
29
+ rspec
30
+ sqlite3
data/Manifest ADDED
@@ -0,0 +1,4 @@
1
+ README.rdoc
2
+ Rakefile
3
+ lib/reversable_data_migration.rb
4
+ Manifest
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # Reversible 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
+ ```ruby
8
+ class RemoveStateFromProduct < ActiveRecord::Migration
9
+ def self.up
10
+ backup_data = []
11
+ Product.all.each do |product|
12
+ backup_data << {:id => product.id, :state => product.state}
13
+ end
14
+ backup backup_data
15
+ remove_column :products, :state
16
+ end
17
+ def self.down
18
+ add_column :products, :state, :string
19
+ restore Product
20
+ end
21
+ end
22
+ ```
23
+ ## Installing
24
+
25
+ gem install reversible_data_migration
26
+
27
+ ## Rails 2 & 3 supported
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,94 @@
1
+ module ReversibleDataMigration
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, ReversibleDataMigration)
92
+ else
93
+ ActiveRecord::Migration.send(:include, ReversibleDataMigration)
94
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "reversible_data_migration"
6
+ s.version = File.open(File.expand_path("../VERSION", __FILE__)).read
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Tom Maeckelberghe"]
9
+ s.email = ["tom.maeckelberghe@gmail.com"]
10
+ s.homepage = "https://github.com/tomkurt/reversible_data_migration"
11
+ s.summary = %q{Makes activerecord data migrations reversible.}
12
+ s.description = %q{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.}
13
+
14
+ s.add_development_dependency "rspec"
15
+ s.add_development_dependency "sqlite3"
16
+ s.add_development_dependency "activerecord", "2.3.14"
17
+
18
+ s.rubyforge_project = "reversible_migration_helper"
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+ end
@@ -0,0 +1,153 @@
1
+ require 'spec_helper'
2
+ module ReversibleDataMigration
3
+ RAILS_ROOT = File.dirname(__FILE__)
4
+ end
5
+
6
+ describe ReversibleDataMigration 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 'reversible_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,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: reversible_data_migration
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 1
10
+ version: 0.1.1
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
+ - !ruby/object:Gem::Dependency
49
+ name: activerecord
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - "="
55
+ - !ruby/object:Gem::Version
56
+ hash: 31
57
+ segments:
58
+ - 2
59
+ - 3
60
+ - 14
61
+ version: 2.3.14
62
+ type: :development
63
+ version_requirements: *id003
64
+ 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.
65
+ email:
66
+ - tom.maeckelberghe@gmail.com
67
+ executables: []
68
+
69
+ extensions: []
70
+
71
+ extra_rdoc_files: []
72
+
73
+ files:
74
+ - .gitignore
75
+ - Gemfile
76
+ - Gemfile.lock
77
+ - Manifest
78
+ - README.md
79
+ - Rakefile
80
+ - VERSION
81
+ - lib/reversible_data_migration.rb
82
+ - reversable_data_migration.gemspec
83
+ - spec/backup_spec.rb
84
+ - spec/spec_helper.rb
85
+ homepage: https://github.com/tomkurt/reversible_data_migration
86
+ licenses: []
87
+
88
+ post_install_message:
89
+ rdoc_options: []
90
+
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ hash: 3
99
+ segments:
100
+ - 0
101
+ version: "0"
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ hash: 3
108
+ segments:
109
+ - 0
110
+ version: "0"
111
+ requirements: []
112
+
113
+ rubyforge_project: reversible_migration_helper
114
+ rubygems_version: 1.8.10
115
+ signing_key:
116
+ specification_version: 3
117
+ summary: Makes activerecord data migrations reversible.
118
+ test_files: []
119
+