tush 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +86 -0
- data/LICENSE.txt +20 -0
- data/README.md +60 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/lib/tush.rb +12 -0
- data/lib/tush/exporter.rb +27 -0
- data/lib/tush/helpers/association_helpers.rb +92 -0
- data/lib/tush/importer.rb +91 -0
- data/lib/tush/model_store.rb +58 -0
- data/lib/tush/model_wrapper.rb +107 -0
- data/spec/association_helpers_spec.rb +81 -0
- data/spec/exporter_spec.rb +39 -0
- data/spec/helper.rb +40 -0
- data/spec/importer_spec.rb +177 -0
- data/spec/model_store_spec.rb +32 -0
- data/spec/model_wrapper_spec.rb +119 -0
- data/spec/support/exported_data.json +1 -0
- data/spec/support/schema.rb +88 -0
- data/tush.gemspec +102 -0
- metadata +299 -0
data/.document
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
tush
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-1.9.3-p327
|
data/Gemfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
gem "activerecord"
|
9
|
+
gem "activesupport"
|
10
|
+
gem "shoulda", ">= 0"
|
11
|
+
gem "rdoc", "~> 3.12"
|
12
|
+
gem "bundler"
|
13
|
+
gem "ruby_deep_clone"
|
14
|
+
gem "sneaky-save", :git => "git@github.com:partyearth/sneaky-save.git"
|
15
|
+
|
16
|
+
group :development, :test do
|
17
|
+
gem 'simplecov'
|
18
|
+
gem "awesome_print"
|
19
|
+
gem "pry"
|
20
|
+
gem "pry-nav"
|
21
|
+
gem "rspec"
|
22
|
+
gem "sqlite3"
|
23
|
+
gem "jeweler", "~> 1.8.4"
|
24
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
GIT
|
2
|
+
remote: git@github.com:partyearth/sneaky-save.git
|
3
|
+
revision: b000d4f941fc220ea9f4686b99753712b103c853
|
4
|
+
specs:
|
5
|
+
sneaky-save (0.0.4)
|
6
|
+
activerecord (>= 3.2.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activemodel (3.2.13)
|
12
|
+
activesupport (= 3.2.13)
|
13
|
+
builder (~> 3.0.0)
|
14
|
+
activerecord (3.2.13)
|
15
|
+
activemodel (= 3.2.13)
|
16
|
+
activesupport (= 3.2.13)
|
17
|
+
arel (~> 3.0.2)
|
18
|
+
tzinfo (~> 0.3.29)
|
19
|
+
activesupport (3.2.13)
|
20
|
+
i18n (= 0.6.1)
|
21
|
+
multi_json (~> 1.0)
|
22
|
+
arel (3.0.2)
|
23
|
+
awesome_print (1.1.0)
|
24
|
+
builder (3.0.4)
|
25
|
+
coderay (1.0.9)
|
26
|
+
diff-lcs (1.2.4)
|
27
|
+
git (1.2.5)
|
28
|
+
i18n (0.6.1)
|
29
|
+
jeweler (1.8.4)
|
30
|
+
bundler (~> 1.0)
|
31
|
+
git (>= 1.2.5)
|
32
|
+
rake
|
33
|
+
rdoc
|
34
|
+
json (1.7.7)
|
35
|
+
method_source (0.8.1)
|
36
|
+
multi_json (1.7.3)
|
37
|
+
pry (0.9.12.1)
|
38
|
+
coderay (~> 1.0.5)
|
39
|
+
method_source (~> 0.8)
|
40
|
+
slop (~> 3.4)
|
41
|
+
pry-nav (0.2.3)
|
42
|
+
pry (~> 0.9.10)
|
43
|
+
rake (10.0.4)
|
44
|
+
rdoc (3.12.2)
|
45
|
+
json (~> 1.4)
|
46
|
+
rspec (2.13.0)
|
47
|
+
rspec-core (~> 2.13.0)
|
48
|
+
rspec-expectations (~> 2.13.0)
|
49
|
+
rspec-mocks (~> 2.13.0)
|
50
|
+
rspec-core (2.13.1)
|
51
|
+
rspec-expectations (2.13.0)
|
52
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
53
|
+
rspec-mocks (2.13.1)
|
54
|
+
ruby_deep_clone (0.3.0)
|
55
|
+
shoulda (3.5.0)
|
56
|
+
shoulda-context (~> 1.0, >= 1.0.1)
|
57
|
+
shoulda-matchers (>= 1.4.1, < 3.0)
|
58
|
+
shoulda-context (1.1.1)
|
59
|
+
shoulda-matchers (2.1.0)
|
60
|
+
activesupport (>= 3.0.0)
|
61
|
+
simplecov (0.7.1)
|
62
|
+
multi_json (~> 1.0)
|
63
|
+
simplecov-html (~> 0.7.1)
|
64
|
+
simplecov-html (0.7.1)
|
65
|
+
slop (3.4.4)
|
66
|
+
sqlite3 (1.3.7)
|
67
|
+
tzinfo (0.3.37)
|
68
|
+
|
69
|
+
PLATFORMS
|
70
|
+
ruby
|
71
|
+
|
72
|
+
DEPENDENCIES
|
73
|
+
activerecord
|
74
|
+
activesupport
|
75
|
+
awesome_print
|
76
|
+
bundler
|
77
|
+
jeweler (~> 1.8.4)
|
78
|
+
pry
|
79
|
+
pry-nav
|
80
|
+
rdoc (~> 3.12)
|
81
|
+
rspec
|
82
|
+
ruby_deep_clone
|
83
|
+
shoulda
|
84
|
+
simplecov
|
85
|
+
sneaky-save!
|
86
|
+
sqlite3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 David Huie
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# Tush
|
2
|
+
|
3
|
+
Tush is a gem for migrating database rows between applications with ActiveRecord,
|
4
|
+
while preserving *all associations.*
|
5
|
+
|
6
|
+
## Installing Tush
|
7
|
+
|
8
|
+
If you're using Rails, just add the following to your Gemfile:
|
9
|
+
```
|
10
|
+
gem 'tush'
|
11
|
+
```
|
12
|
+
|
13
|
+
## Tush Exports
|
14
|
+
|
15
|
+
Data is fed to the importer by first creating a JSON export of your
|
16
|
+
rows. This is done by feeding your ActiveRecord model instances to the
|
17
|
+
exporter:
|
18
|
+
```ruby
|
19
|
+
model_instance1 = ActiveRecordModel.create
|
20
|
+
json_export = Tush::Exporter.new([model_instance1]).export_json
|
21
|
+
```
|
22
|
+
This will immediately scan each input model instance and recursively
|
23
|
+
add any associated models to the export. If `model_instance1` has an
|
24
|
+
association with `model_instance2` and `model_instance2` has an
|
25
|
+
association with `model_instance3`, *all 3* model instances will be
|
26
|
+
included in the export.
|
27
|
+
|
28
|
+
## Tush Imports
|
29
|
+
|
30
|
+
Using a JSON export created with `Tush::Exporter`, we can initialize
|
31
|
+
an import, usually in a different application that shares the same
|
32
|
+
ActiveRecord models,like this:
|
33
|
+
```ruby
|
34
|
+
importer = Tush::Importer.new_from_json(json_export)
|
35
|
+
```
|
36
|
+
When we want to create the new models, and therefore importing all
|
37
|
+
exported rows from our other application, we run
|
38
|
+
```ruby
|
39
|
+
importer.create_models!
|
40
|
+
```
|
41
|
+
And to update all foreign keys to be accurate in the new application,
|
42
|
+
run
|
43
|
+
```ruby
|
44
|
+
importer.update_foreign_keys!
|
45
|
+
```
|
46
|
+
|
47
|
+
## Contributing to Tush
|
48
|
+
|
49
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
50
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
51
|
+
* Fork the project.
|
52
|
+
* Start a feature/bugfix branch.
|
53
|
+
* Commit and push until you are happy with your contribution.
|
54
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
55
|
+
* Tush has *100% test coverage*; keep it that way.
|
56
|
+
|
57
|
+
## Copyright
|
58
|
+
|
59
|
+
Copyright (c) 2013 NationBuilder. See LICENSE.txt for
|
60
|
+
further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
gem.name = "tush"
|
17
|
+
gem.homepage = "http://github.com/3dna/tush"
|
18
|
+
gem.license = "MIT"
|
19
|
+
gem.summary = "Simplified ActiveRecord data migrations between app instances"
|
20
|
+
gem.description = "Simplified ActiveRecord data migrations between app instances"
|
21
|
+
gem.email = "david@nationbuilder.com"
|
22
|
+
gem.authors = ["David Huie", "Lauren Mermel"]
|
23
|
+
end
|
24
|
+
Jeweler::RubygemsDotOrgTasks.new
|
25
|
+
|
26
|
+
require 'rspec/core/rake_task'
|
27
|
+
RSpec::Core::RakeTask.new(:spec)
|
28
|
+
task :default => :spec
|
29
|
+
|
30
|
+
require 'rdoc/task'
|
31
|
+
Rake::RDocTask.new do |rdoc|
|
32
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
33
|
+
|
34
|
+
rdoc.rdoc_dir = 'rdoc'
|
35
|
+
rdoc.title = "tush #{version}"
|
36
|
+
rdoc.rdoc_files.include('README*')
|
37
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
38
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.1
|
data/lib/tush.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Tush
|
4
|
+
|
5
|
+
# This class exports a collection of ModelStores as JSON.
|
6
|
+
class Exporter
|
7
|
+
|
8
|
+
attr_accessor :data
|
9
|
+
|
10
|
+
def initialize(model_instances, opts={})
|
11
|
+
blacklisted_models = opts[:blacklisted_models] || []
|
12
|
+
copy_only_models = opts[:copy_only_models] || []
|
13
|
+
|
14
|
+
model_store = ModelStore.new(:blacklisted_models => blacklisted_models,
|
15
|
+
:copy_only_models => copy_only_models)
|
16
|
+
model_store.push_array(model_instances)
|
17
|
+
|
18
|
+
self.data = model_store.export
|
19
|
+
end
|
20
|
+
|
21
|
+
def export_json
|
22
|
+
self.data.to_json
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Tush
|
2
|
+
|
3
|
+
class AssociationHelpers
|
4
|
+
|
5
|
+
def self.relation_infos(relation_type, klass)
|
6
|
+
if klass.is_a?(String)
|
7
|
+
klass = klass.constantize
|
8
|
+
end
|
9
|
+
|
10
|
+
klass.reflect_on_all_associations(relation_type)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.model_relation_info(model)
|
14
|
+
relation_infos = {}
|
15
|
+
|
16
|
+
SUPPORTED_ASSOCIATIONS.each do |association_type|
|
17
|
+
relation_infos[association_type] = self.relation_infos(association_type, model)
|
18
|
+
end
|
19
|
+
|
20
|
+
relation_infos
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.model_to_relation_infos(models)
|
24
|
+
models = models.uniq
|
25
|
+
model_to_relation_infos_hash = {}
|
26
|
+
|
27
|
+
models.each do |model|
|
28
|
+
model_to_relation_infos_hash[model] = self.model_relation_info(model)
|
29
|
+
end
|
30
|
+
|
31
|
+
model_to_relation_infos_hash
|
32
|
+
end
|
33
|
+
|
34
|
+
# Determine the class the foreign key points to
|
35
|
+
def self.class_for_foreign_key(association_info)
|
36
|
+
if association_info.macro == :belongs_to
|
37
|
+
association_info.class_name.constantize
|
38
|
+
else
|
39
|
+
association_info.active_record
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Determine the class that actually has the foreign key.
|
44
|
+
def self.class_with_foreign_key(association_info)
|
45
|
+
if association_info.macro == :belongs_to
|
46
|
+
association_info.active_record
|
47
|
+
else
|
48
|
+
# has_one and has_many keys are stored in a model that's
|
49
|
+
# different than the one they're declared in.
|
50
|
+
association_info.class_name.constantize
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# This method locates all foreign key columns for a a list of model classes
|
55
|
+
# for foreign keys declared within the list of models classes.
|
56
|
+
def self.create_foreign_key_mapping(model_classes)
|
57
|
+
model_to_foreign_keys = {}
|
58
|
+
model_classes.each do |model_class|
|
59
|
+
model_to_foreign_keys[model_class] = []
|
60
|
+
end
|
61
|
+
|
62
|
+
model_to_relation_infos(model_classes).each do |model, relation_infos|
|
63
|
+
SUPPORTED_ASSOCIATIONS.each do |association_type|
|
64
|
+
|
65
|
+
associations = relation_infos[association_type]
|
66
|
+
|
67
|
+
associations.each do |association|
|
68
|
+
klass = self.class_with_foreign_key(association)
|
69
|
+
|
70
|
+
# An association with a class that wasn't
|
71
|
+
# included in our list of model_classes might be found.
|
72
|
+
unless model_to_foreign_keys.keys.include?(klass)
|
73
|
+
model_to_foreign_keys[klass] = []
|
74
|
+
end
|
75
|
+
|
76
|
+
association_hash = { :foreign_key => association.foreign_key,
|
77
|
+
:class => self.class_for_foreign_key(association) }
|
78
|
+
|
79
|
+
unless model_to_foreign_keys[klass].include?(association_hash)
|
80
|
+
model_to_foreign_keys[klass] << association_hash
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
model_to_foreign_keys
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'tush/model_store'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Tush
|
5
|
+
|
6
|
+
# This class takes in a tush export and imports it into ActiveRecord.
|
7
|
+
class Importer
|
8
|
+
|
9
|
+
class NonUniqueWrapperError < RuntimeError; end
|
10
|
+
class InvalidWrapperError < RuntimeError; end
|
11
|
+
|
12
|
+
attr_accessor(:data,
|
13
|
+
:imported_model_wrappers,
|
14
|
+
:model_to_attribute_blacklist)
|
15
|
+
|
16
|
+
def initialize(exported_data)
|
17
|
+
self.data = exported_data
|
18
|
+
self.imported_model_wrappers = []
|
19
|
+
self.model_to_attribute_blacklist = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.new_from_json_file(json_path)
|
23
|
+
unparsed_json = File.read(json_path)
|
24
|
+
self.new(JSON.parse(unparsed_json))
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.new_from_json(unparsed_json)
|
28
|
+
self.new(JSON.parse(unparsed_json))
|
29
|
+
end
|
30
|
+
|
31
|
+
def create_models!
|
32
|
+
model_wrappers = self.data["model_wrappers"]
|
33
|
+
|
34
|
+
model_wrappers.each do |model_wrapper|
|
35
|
+
model_class = model_wrapper["model_class"].constantize
|
36
|
+
imported_model_wrapper =
|
37
|
+
ModelWrapper.new(:model_class => model_class.to_s,
|
38
|
+
:model_attributes => model_wrapper["model_attributes"],
|
39
|
+
:blacklisted_attributes => self.model_to_attribute_blacklist[model_class])
|
40
|
+
|
41
|
+
imported_model_wrapper.create_copy
|
42
|
+
|
43
|
+
self.imported_model_wrappers << imported_model_wrapper
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_wrapper_by_class_and_old_id(klass, old_id)
|
48
|
+
wrappers = self.imported_model_wrappers.select do |wrapper|
|
49
|
+
(wrapper.model_class == klass) and (wrapper.original_db_id == old_id)
|
50
|
+
end
|
51
|
+
|
52
|
+
wrappers[0]
|
53
|
+
end
|
54
|
+
|
55
|
+
# This method updates stale foreign keys after the new models have been created.
|
56
|
+
def update_foreign_keys!
|
57
|
+
models = self.imported_model_wrappers.map { |wrapper| wrapper.model_class }
|
58
|
+
model_to_foreign_keys = AssociationHelpers.create_foreign_key_mapping(models)
|
59
|
+
|
60
|
+
imported_model_wrappers.each do |wrapper|
|
61
|
+
foreign_keys = model_to_foreign_keys[wrapper.model_class]
|
62
|
+
|
63
|
+
foreign_keys.each do |foreign_key_info|
|
64
|
+
match = self.find_wrapper_by_class_and_old_id(foreign_key_info[:class],
|
65
|
+
wrapper.model_attributes[foreign_key_info[:foreign_key]])
|
66
|
+
|
67
|
+
if match
|
68
|
+
new_id = match.new_model.send(match.original_db_key)
|
69
|
+
else
|
70
|
+
# If we don't have a model wrapper (like in the case of a copy only model),
|
71
|
+
# remove the id
|
72
|
+
new_id = nil
|
73
|
+
end
|
74
|
+
|
75
|
+
ActiveRecord::Base.transaction(:requires_new => true) do
|
76
|
+
begin
|
77
|
+
wrapper.new_model.update_column(foreign_key_info[:foreign_key],
|
78
|
+
new_id)
|
79
|
+
# If the column has a not null restraint, keep the original value
|
80
|
+
rescue ActiveRecord::StatementInvalid
|
81
|
+
raise ActiveRecord::Rollback
|
82
|
+
end
|
83
|
+
|
84
|
+
wrapper.new_model_attributes[foreign_key_info[:foreign_key]] = new_id
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|