tush 0.2.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/.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
|