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 +4 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +43 -0
- data/Manifest +4 -0
- data/README.rdoc +21 -0
- data/Rakefile +2 -0
- data/VERSION +1 -0
- data/lib/reversable_data_migration.rb +94 -0
- data/spec/backup_spec.rb +153 -0
- data/spec/spec_helper.rb +38 -0
- metadata +102 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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
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
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
|
data/spec/backup_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|