reversible_data_migration 0.1.1

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,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
+