immigrant 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/LICENSE.txt +20 -0
- data/README.rdoc +40 -0
- data/Rakefile +13 -0
- data/lib/generators/USAGE +19 -0
- data/lib/generators/immigration_generator.rb +20 -0
- data/lib/generators/templates/immigration-pre-3.1.rb +13 -0
- data/lib/generators/templates/immigration.rb +7 -0
- data/lib/immigrant/foreign_key_definition.rb +38 -0
- data/lib/immigrant/loader.rb +7 -0
- data/lib/immigrant/railtie.rb +13 -0
- data/lib/immigrant.rb +141 -0
- data/test/helper.rb +20 -0
- data/test/immigrant_test.rb +314 -0
- metadata +113 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Jon Jensen
|
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.rdoc
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
= Immigrant
|
2
|
+
|
3
|
+
Immigrant gives {Foreigner}[https://github.com/matthuhiggins/foreigner] a
|
4
|
+
migration generator so you can effortlessly add missing foreign keys. This is
|
5
|
+
particularly helpful when you decide to add keys to an established Rails app.
|
6
|
+
|
7
|
+
Like Foreigner, Immigrant requires Rails 3.0 or greater.
|
8
|
+
|
9
|
+
== Installation
|
10
|
+
|
11
|
+
Add the following to your Gemfile:
|
12
|
+
|
13
|
+
gem 'immigrant'
|
14
|
+
|
15
|
+
== Usage
|
16
|
+
|
17
|
+
rails generate immigration AddKeys
|
18
|
+
|
19
|
+
This will create a migration named AddKeys which will have add_foreign_key
|
20
|
+
statements for any missing foreign keys. Immigrant infers missing ones by
|
21
|
+
evaluating the associations in your models (e.g. belongs_to, has_many, etc.).
|
22
|
+
Only missing keys will be added; existing ones will never be altered or
|
23
|
+
removed.
|
24
|
+
|
25
|
+
== Considerations
|
26
|
+
|
27
|
+
If the data in your tables is bad, then the migration will fail to run
|
28
|
+
(obviously). IOW, ensure you don't have orphaned records *before* you try to
|
29
|
+
add foreign keys.
|
30
|
+
|
31
|
+
== Known Issues
|
32
|
+
|
33
|
+
Immigrant currently only looks for foreign keys in ActiveRecord::Base's
|
34
|
+
database. So if a model is using a different database connection and it has
|
35
|
+
foreign keys, Immigrant will incorrectly include them again in the generated
|
36
|
+
migration.
|
37
|
+
|
38
|
+
== License
|
39
|
+
|
40
|
+
Copyright (c) 2012 Jon Jensen, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
desc 'Default: run unit tests.'
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
require 'rake/testtask'
|
7
|
+
desc 'Test the immigrant plugin.'
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.libs << 'test'
|
11
|
+
t.pattern = 'test/**/*_test.rb'
|
12
|
+
t.verbose = true
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Description:
|
2
|
+
Creates a new foreign key migration based on your current associations.
|
3
|
+
Pass the migration name, either CamelCased or under_scored.
|
4
|
+
|
5
|
+
A migration class is generated in db/migrate prefixed by a timestamp of the
|
6
|
+
current date and time. It will contain add_foreign_key calls to create any
|
7
|
+
foreign keys that do not already exist (inferred from your model
|
8
|
+
associations and current foreign keys in the database). If there are no
|
9
|
+
missing foreign keys, no migration will be created.
|
10
|
+
|
11
|
+
Example:
|
12
|
+
`rails generate immigration AddMissingForeignKeys`
|
13
|
+
|
14
|
+
If the current date is Apr 1, 2012 and the current time 02:03:04, this
|
15
|
+
creates the AddMissingForeignKeys migration
|
16
|
+
db/migrate/20120401020304_add_missing_foreign_keys.rb with appropriate
|
17
|
+
add_foreign_key calls in the Change migration. If on Rails < 3.1, they
|
18
|
+
will be in the Up migration, and corresponding remove_foreign_key calls
|
19
|
+
will be in the Down migration.
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
class ImmigrationGenerator < ActiveRecord::Generators::Base
|
4
|
+
def create_immigration_file
|
5
|
+
@keys, warnings = Immigrant.infer_keys
|
6
|
+
warnings.values.each{ |warning| $stderr.puts "WARNING: #{warning}" }
|
7
|
+
@keys.each do |key|
|
8
|
+
next unless key.options[:dependent] == :delete
|
9
|
+
$stderr.puts "NOTICE: #{key.options[:name]} has ON DELETE CASCADE. You should remove the :dependent option from the association to take advantage of this."
|
10
|
+
end
|
11
|
+
if @keys.present?
|
12
|
+
template = ActiveRecord::VERSION::STRING < "3.1." ? "immigration-pre-3.1.rb" : "immigration.rb"
|
13
|
+
migration_template template, "db/migrate/#{file_name}.rb"
|
14
|
+
else
|
15
|
+
puts "Nothing to do"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
|
20
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
<% @keys.each do |key| -%>
|
4
|
+
<%= key.to_ruby(:add) %>
|
5
|
+
<%- end -%>
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.down
|
9
|
+
<% @keys.each do |key| -%>
|
10
|
+
<%= key.to_ruby(:remove) %>
|
11
|
+
<%- end -%>
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Immigrant
|
2
|
+
# add some useful stuff to foreigner's ForeignKeyDefinition
|
3
|
+
# TODO: get it in foreigner so we don't need to monkey patch
|
4
|
+
module ForeignKeyDefinition
|
5
|
+
def initialize(from_table, to_table, options, *args)
|
6
|
+
options ||= {}
|
7
|
+
options[:name] ||= "#{from_table}_#{options[:column]}_fk"
|
8
|
+
super(from_table, to_table, options, *args)
|
9
|
+
end
|
10
|
+
|
11
|
+
def hash_key
|
12
|
+
[from_table, options[:column]]
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_ruby(action = :add)
|
16
|
+
if action == :add
|
17
|
+
# not DRY ... guts of this are copied from Foreigner :(
|
18
|
+
parts = [ ('add_foreign_key ' + from_table.inspect) ]
|
19
|
+
parts << to_table.inspect
|
20
|
+
parts << (':name => ' + options[:name].inspect)
|
21
|
+
|
22
|
+
if options[:column] != "#{to_table.singularize}_id"
|
23
|
+
parts << (':column => ' + options[:column].inspect)
|
24
|
+
end
|
25
|
+
if options[:primary_key] != 'id'
|
26
|
+
parts << (':primary_key => ' + options[:primary_key].inspect)
|
27
|
+
end
|
28
|
+
if options[:dependent].present?
|
29
|
+
parts << (':dependent => ' + options[:dependent].inspect)
|
30
|
+
end
|
31
|
+
parts.join(', ')
|
32
|
+
else
|
33
|
+
"remove_foreign_key #{from_table.inspect}, " \
|
34
|
+
":name => #{options[:name].inspect}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Immigrant
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
initializer 'immigrant.load' do
|
4
|
+
# TODO: implement hook in Foreigner and use that instead
|
5
|
+
ActiveSupport.on_load :active_record do
|
6
|
+
Immigrant.load
|
7
|
+
end
|
8
|
+
end
|
9
|
+
generators do
|
10
|
+
require "generators/immigration_generator"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/immigrant.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'foreigner'
|
3
|
+
|
4
|
+
module Immigrant
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
autoload :Loader
|
7
|
+
autoload :ForeignKeyDefinition
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def infer_keys(db_keys = current_foreign_keys, classes = model_classes)
|
11
|
+
database_keys = db_keys.inject({}) { |hash, foreign_key|
|
12
|
+
hash[foreign_key.hash_key] = foreign_key
|
13
|
+
hash
|
14
|
+
}
|
15
|
+
model_keys, warnings = model_keys(classes)
|
16
|
+
new_keys = []
|
17
|
+
model_keys.keys.each do |hash_key|
|
18
|
+
foreign_key = model_keys[hash_key]
|
19
|
+
# if the foreign key exists in the db, we call it good (even if
|
20
|
+
# the name is different or :dependent doesn't match), though
|
21
|
+
# we do warn on clearly broken stuff
|
22
|
+
if current_key = database_keys[hash_key]
|
23
|
+
if current_key.to_table != foreign_key.to_table || current_key.options[:primary_key] != foreign_key.options[:primary_key]
|
24
|
+
warnings[hash_key] = "Skipping #{foreign_key.from_table}.#{foreign_key.options[:column]}: its association references a different key/table than its current foreign key"
|
25
|
+
end
|
26
|
+
else
|
27
|
+
new_keys << foreign_key
|
28
|
+
end
|
29
|
+
end
|
30
|
+
[new_keys.sort_by{ |key| key.options[:name] }, warnings]
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def current_foreign_keys
|
36
|
+
ActiveRecord::Base.connection.tables.map{ |table|
|
37
|
+
ActiveRecord::Base.connection.foreign_keys(table)
|
38
|
+
}.flatten
|
39
|
+
end
|
40
|
+
|
41
|
+
def model_classes
|
42
|
+
classes = []
|
43
|
+
pattern = /^\s*(has_one|has_many|has_and_belongs_to_many|belongs_to)\s/
|
44
|
+
Dir['app/models/*.rb'].each do |model|
|
45
|
+
class_name = model.sub(/\A.*\/(.*?)\.rb\z/, '\1').camelize
|
46
|
+
begin
|
47
|
+
klass = class_name.constantize
|
48
|
+
rescue SyntaxError, LoadError
|
49
|
+
if File.read(model) =~ pattern
|
50
|
+
raise "unable to load #{class_name} and its associations"
|
51
|
+
end
|
52
|
+
next
|
53
|
+
end
|
54
|
+
classes << klass if klass < ActiveRecord::Base
|
55
|
+
end
|
56
|
+
classes
|
57
|
+
end
|
58
|
+
|
59
|
+
def model_keys(classes)
|
60
|
+
# see what the models say there should be
|
61
|
+
foreign_keys = {}
|
62
|
+
warnings = {}
|
63
|
+
classes.map{ |klass|
|
64
|
+
foreign_keys_for(klass)
|
65
|
+
}.flatten.uniq.each do |foreign_key|
|
66
|
+
# we may have inferred it from several different places, e.g.
|
67
|
+
# Bar.belongs_to :foo
|
68
|
+
# Foo.has_many :bars
|
69
|
+
# Foo.has_many :bazzes, :class_name => Bar
|
70
|
+
# we need to make sure everything is legit and see if any of them
|
71
|
+
# specify :dependent => :delete
|
72
|
+
if current_key = foreign_keys[foreign_key.hash_key]
|
73
|
+
if current_key.to_table != foreign_key.to_table || current_key.options[:primary_key] != foreign_key.options[:primary_key]
|
74
|
+
warnings[foreign_key.hash_key] ||= "Skipping #{foreign_key.from_table}.#{foreign_key.options[:column]}: it has multiple associations referencing different keys/tables."
|
75
|
+
next
|
76
|
+
else
|
77
|
+
next unless foreign_key.options[:dependent]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
foreign_keys[foreign_key.hash_key] = foreign_key
|
81
|
+
end
|
82
|
+
warnings.keys.each { |hash_key| foreign_keys.delete(hash_key) }
|
83
|
+
[foreign_keys, warnings]
|
84
|
+
end
|
85
|
+
|
86
|
+
def foreign_keys_for(klass)
|
87
|
+
fk_method = ActiveRecord::VERSION::STRING < '3.1.' ? :primary_key_name : :foreign_key
|
88
|
+
|
89
|
+
klass.reflections.values.reject{ |reflection|
|
90
|
+
# some associations can just be ignored, since:
|
91
|
+
# 1. we aren't going to parse SQL
|
92
|
+
# 2. foreign keys for :through associations will be handled by their
|
93
|
+
# component has_one/has_many/belongs_to associations
|
94
|
+
# 3. :polymorphic(/:as) associations can't have foreign keys
|
95
|
+
(reflection.options.keys & [:finder_sql, :through, :polymorphic, :as]).present?
|
96
|
+
}.map { |reflection|
|
97
|
+
begin
|
98
|
+
case reflection.macro
|
99
|
+
when :belongs_to
|
100
|
+
Foreigner::ConnectionAdapters::ForeignKeyDefinition.new(
|
101
|
+
klass.table_name, reflection.klass.table_name,
|
102
|
+
:column => reflection.send(fk_method).to_s,
|
103
|
+
:primary_key => reflection.klass.primary_key.to_s,
|
104
|
+
# although belongs_to can specify :dependent, it doesn't make
|
105
|
+
# sense from a foreign key perspective
|
106
|
+
:dependent => nil
|
107
|
+
)
|
108
|
+
when :has_one, :has_many
|
109
|
+
Foreigner::ConnectionAdapters::ForeignKeyDefinition.new(
|
110
|
+
reflection.klass.table_name, klass.table_name,
|
111
|
+
:column => reflection.send(fk_method).to_s,
|
112
|
+
:primary_key => klass.primary_key.to_s,
|
113
|
+
:dependent => [:delete, :delete_all].include?(reflection.options[:dependent]) && reflection.options[:conditions].nil? ? :delete : nil
|
114
|
+
)
|
115
|
+
when :has_and_belongs_to_many
|
116
|
+
[
|
117
|
+
Foreigner::ConnectionAdapters::ForeignKeyDefinition.new(
|
118
|
+
reflection.options[:join_table], klass.table_name,
|
119
|
+
:column => reflection.send(fk_method).to_s,
|
120
|
+
:primary_key => klass.primary_key.to_s,
|
121
|
+
:dependent => nil
|
122
|
+
),
|
123
|
+
Foreigner::ConnectionAdapters::ForeignKeyDefinition.new(
|
124
|
+
reflection.options[:join_table], reflection.klass.table_name,
|
125
|
+
:column => reflection.association_foreign_key.to_s,
|
126
|
+
:primary_key => reflection.klass.primary_key.to_s,
|
127
|
+
:dependent => nil
|
128
|
+
)
|
129
|
+
]
|
130
|
+
end
|
131
|
+
rescue NameError # e.g. belongs_to :oops_this_is_not_a_table
|
132
|
+
[]
|
133
|
+
end
|
134
|
+
}.flatten
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
require 'immigrant/loader'
|
141
|
+
require 'immigrant/railtie' if defined?(Rails)
|
data/test/helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
Bundler.require(:default)
|
3
|
+
|
4
|
+
require 'test/unit'
|
5
|
+
require 'active_record'
|
6
|
+
|
7
|
+
require 'foreigner'
|
8
|
+
class Foreigner::Adapter
|
9
|
+
def self.configured_name; "dummy_adapter"; end
|
10
|
+
end
|
11
|
+
Foreigner.load
|
12
|
+
|
13
|
+
require 'immigrant'
|
14
|
+
Immigrant.load
|
15
|
+
|
16
|
+
module TestMethods
|
17
|
+
def foreign_key_definition(*args)
|
18
|
+
Foreigner::ConnectionAdapters::ForeignKeyDefinition.new(*args)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class ImmigrantTest < ActiveSupport::TestCase
|
4
|
+
include TestMethods
|
5
|
+
|
6
|
+
class MockModel < ActiveRecord::Base
|
7
|
+
self.abstract_class = true
|
8
|
+
class << self
|
9
|
+
def primary_key
|
10
|
+
connection.primary_key(table_name)
|
11
|
+
end
|
12
|
+
def connection
|
13
|
+
@connection ||= MockConnection.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class MockConnection
|
19
|
+
def supports_primary_key? # AR <3.2
|
20
|
+
true
|
21
|
+
end
|
22
|
+
def primary_key(table)
|
23
|
+
table !~ /s_.*s\z/ ? 'id' : nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def teardown
|
28
|
+
subclasses = ActiveSupport::DescendantsTracker.direct_descendants(MockModel)
|
29
|
+
subclasses.each do |subclass|
|
30
|
+
ImmigrantTest.send(:remove_const, subclass.to_s.sub(/.*::/, ''))
|
31
|
+
end
|
32
|
+
subclasses.replace([])
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# basic scenarios
|
37
|
+
|
38
|
+
test 'belongs_to should generate a foreign key' do
|
39
|
+
class Author < MockModel; end
|
40
|
+
class Book < MockModel
|
41
|
+
belongs_to :guy, :class_name => 'Author', :foreign_key => 'author_id'
|
42
|
+
end
|
43
|
+
|
44
|
+
assert_equal(
|
45
|
+
[foreign_key_definition(
|
46
|
+
'books', 'authors',
|
47
|
+
:column => 'author_id', :primary_key => 'id', :dependent => nil
|
48
|
+
)],
|
49
|
+
Immigrant.infer_keys([], [Author, Book]).first
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
test 'has_one should generate a foreign key' do
|
54
|
+
class Author < MockModel
|
55
|
+
has_one :piece_de_resistance, :class_name => 'Book', :order => "id DESC"
|
56
|
+
end
|
57
|
+
class Book < MockModel; end
|
58
|
+
|
59
|
+
assert_equal(
|
60
|
+
[foreign_key_definition(
|
61
|
+
'books', 'authors',
|
62
|
+
:column => 'author_id', :primary_key => 'id', :dependent => nil
|
63
|
+
)],
|
64
|
+
Immigrant.infer_keys([], [Author, Book]).first
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
test 'has_one :dependent => :delete should generate a foreign key with :dependent => :delete' do
|
69
|
+
class Author < MockModel
|
70
|
+
has_one :book, :order => "id DESC", :dependent => :delete
|
71
|
+
end
|
72
|
+
class Book < MockModel; end
|
73
|
+
|
74
|
+
assert_equal(
|
75
|
+
[foreign_key_definition(
|
76
|
+
'books', 'authors',
|
77
|
+
:column => 'author_id', :primary_key => 'id', :dependent => :delete
|
78
|
+
)],
|
79
|
+
Immigrant.infer_keys([], [Author, Book]).first
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
test 'has_many should generate a foreign key' do
|
84
|
+
class Author < MockModel
|
85
|
+
has_many :babies, :class_name => 'Book'
|
86
|
+
end
|
87
|
+
class Book < MockModel; end
|
88
|
+
|
89
|
+
assert_equal(
|
90
|
+
[foreign_key_definition(
|
91
|
+
'books', 'authors',
|
92
|
+
:column => 'author_id', :primary_key => 'id', :dependent => nil
|
93
|
+
)],
|
94
|
+
Immigrant.infer_keys([], [Author, Book]).first
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
test 'has_many :dependent => :delete_all should generate a foreign key with :dependent => :delete' do
|
99
|
+
class Author < MockModel
|
100
|
+
has_many :books, :dependent => :delete_all
|
101
|
+
end
|
102
|
+
class Book < MockModel; end
|
103
|
+
|
104
|
+
assert_equal(
|
105
|
+
[foreign_key_definition(
|
106
|
+
'books', 'authors',
|
107
|
+
:column => 'author_id', :primary_key => 'id', :dependent => :delete
|
108
|
+
)],
|
109
|
+
Immigrant.infer_keys([], [Author, Book]).first
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
test 'has_and_belongs_to_many should generate two foreign keys' do
|
114
|
+
class Author < MockModel
|
115
|
+
has_and_belongs_to_many :fans
|
116
|
+
end
|
117
|
+
class Fan < MockModel; end
|
118
|
+
|
119
|
+
assert_equal(
|
120
|
+
[foreign_key_definition(
|
121
|
+
'authors_fans', 'authors',
|
122
|
+
:column => 'author_id', :primary_key => 'id', :dependent => nil
|
123
|
+
),
|
124
|
+
foreign_key_definition(
|
125
|
+
'authors_fans', 'fans',
|
126
|
+
:column => 'fan_id', :primary_key => 'id', :dependent => nil
|
127
|
+
)],
|
128
|
+
Immigrant.infer_keys([], [Author, Fan]).first
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
test 'conditional has_one/has_many associations should ignore :dependent' do
|
133
|
+
class Author < MockModel
|
134
|
+
has_many :articles, :conditions => "published", :dependent => :delete_all
|
135
|
+
has_one :favorite_book, :class_name => 'Book',
|
136
|
+
:conditions => "most_awesome", :dependent => :delete
|
137
|
+
end
|
138
|
+
class Book < MockModel; end
|
139
|
+
class Article < MockModel; end
|
140
|
+
|
141
|
+
assert_equal(
|
142
|
+
[foreign_key_definition(
|
143
|
+
'articles', 'authors',
|
144
|
+
:column => 'author_id', :primary_key => 'id', :dependent => nil
|
145
|
+
),
|
146
|
+
foreign_key_definition(
|
147
|
+
'books', 'authors',
|
148
|
+
:column => 'author_id', :primary_key => 'id', :dependent => nil
|
149
|
+
)],
|
150
|
+
Immigrant.infer_keys([], [Article, Author, Book]).first
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
# (no) duplication
|
156
|
+
|
157
|
+
test 'STI should not generate duplicate foreign keys' do
|
158
|
+
class Company < MockModel; end
|
159
|
+
class Employee < MockModel
|
160
|
+
belongs_to :company
|
161
|
+
end
|
162
|
+
class Manager < Employee; end
|
163
|
+
|
164
|
+
assert(Manager.reflections.present?)
|
165
|
+
assert_equal(
|
166
|
+
[foreign_key_definition(
|
167
|
+
'employees', 'companies',
|
168
|
+
:column => 'company_id', :primary_key => 'id', :dependent => nil
|
169
|
+
)],
|
170
|
+
Immigrant.infer_keys([], [Company, Employee, Manager]).first
|
171
|
+
)
|
172
|
+
end
|
173
|
+
|
174
|
+
test 'complementary associations should not generate duplicate foreign keys' do
|
175
|
+
class Author < MockModel
|
176
|
+
has_many :books
|
177
|
+
end
|
178
|
+
class Book < MockModel
|
179
|
+
belongs_to :author
|
180
|
+
end
|
181
|
+
|
182
|
+
assert_equal(
|
183
|
+
[foreign_key_definition(
|
184
|
+
'books', 'authors',
|
185
|
+
:column => 'author_id', :primary_key => 'id', :dependent => nil
|
186
|
+
)],
|
187
|
+
Immigrant.infer_keys([], [Author, Book]).first
|
188
|
+
)
|
189
|
+
end
|
190
|
+
|
191
|
+
test 'redundant associations should not generate duplicate foreign keys' do
|
192
|
+
class Author < MockModel
|
193
|
+
has_many :books
|
194
|
+
has_many :favorite_books, :class_name => 'Book', :conditions => "awesome"
|
195
|
+
has_many :bad_books, :class_name => 'Book', :conditions => "amateur_hour"
|
196
|
+
end
|
197
|
+
class Book < MockModel; end
|
198
|
+
|
199
|
+
assert_equal(
|
200
|
+
[foreign_key_definition(
|
201
|
+
'books', 'authors',
|
202
|
+
:column => 'author_id', :primary_key => 'id', :dependent => nil
|
203
|
+
)],
|
204
|
+
Immigrant.infer_keys([], [Author, Book]).first
|
205
|
+
)
|
206
|
+
end
|
207
|
+
|
208
|
+
|
209
|
+
# skipped associations
|
210
|
+
|
211
|
+
test 'associations should not generate foreign keys if they already exist, even if :dependent/name are different' do
|
212
|
+
database_keys = [
|
213
|
+
foreign_key_definition(
|
214
|
+
'articles', 'authors',
|
215
|
+
:column => 'author_id', :primary_key => 'id', :dependent => nil,
|
216
|
+
:name => "doesn't_matter"
|
217
|
+
),
|
218
|
+
foreign_key_definition(
|
219
|
+
'books', 'authors', :column => 'author_id', :primary_key => 'id',
|
220
|
+
:dependent => :delete
|
221
|
+
)
|
222
|
+
]
|
223
|
+
|
224
|
+
class Author < MockModel
|
225
|
+
has_many :articles
|
226
|
+
has_one :favorite_book, :class_name => 'Book',
|
227
|
+
:conditions => "most_awesome"
|
228
|
+
end
|
229
|
+
class Book < MockModel; end
|
230
|
+
class Article < MockModel; end
|
231
|
+
|
232
|
+
assert_equal(
|
233
|
+
[],
|
234
|
+
Immigrant.infer_keys(database_keys, [Article, Author, Book]).first
|
235
|
+
)
|
236
|
+
end
|
237
|
+
|
238
|
+
test 'finder_sql associations should not generate foreign keys' do
|
239
|
+
class Author < MockModel
|
240
|
+
has_many :books, :finder_sql => <<-SQL
|
241
|
+
SELECT *
|
242
|
+
FROM books
|
243
|
+
WHERE author_id = \#{id}
|
244
|
+
ORDER BY RANDOM() LIMIT 5'
|
245
|
+
SQL
|
246
|
+
end
|
247
|
+
class Book < MockModel; end
|
248
|
+
|
249
|
+
assert_equal(
|
250
|
+
[],
|
251
|
+
Immigrant.infer_keys([], [Author, Book]).first
|
252
|
+
)
|
253
|
+
end
|
254
|
+
|
255
|
+
test 'polymorphic associations should not generate foreign keys' do
|
256
|
+
class Property < MockModel
|
257
|
+
belongs_to :owner, :polymorphic => true
|
258
|
+
end
|
259
|
+
class Person < MockModel
|
260
|
+
has_many :properties, :as => :owner
|
261
|
+
end
|
262
|
+
class Corporation < MockModel
|
263
|
+
has_many :properties, :as => :owner
|
264
|
+
end
|
265
|
+
|
266
|
+
assert_equal(
|
267
|
+
[],
|
268
|
+
Immigrant.infer_keys([], [Corporation, Person, Property]).first
|
269
|
+
)
|
270
|
+
end
|
271
|
+
|
272
|
+
test 'has_many :through should not generate foreign keys' do
|
273
|
+
class Author < MockModel
|
274
|
+
has_many :authors_fans
|
275
|
+
has_many :fans, :through => :authors_fans
|
276
|
+
end
|
277
|
+
class AuthorsFan < MockModel
|
278
|
+
belongs_to :author
|
279
|
+
belongs_to :fan
|
280
|
+
end
|
281
|
+
class Fan < MockModel
|
282
|
+
has_many :authors_fans
|
283
|
+
has_many :authors, :through => :authors_fans
|
284
|
+
end
|
285
|
+
|
286
|
+
assert_equal(
|
287
|
+
[foreign_key_definition(
|
288
|
+
'authors_fans', 'authors',
|
289
|
+
:column => 'author_id', :primary_key => 'id', :dependent => nil
|
290
|
+
),
|
291
|
+
foreign_key_definition(
|
292
|
+
'authors_fans', 'fans',
|
293
|
+
:column => 'fan_id', :primary_key => 'id', :dependent => nil
|
294
|
+
)],
|
295
|
+
Immigrant.infer_keys([], [Author, AuthorsFan, Fan]).first
|
296
|
+
)
|
297
|
+
end
|
298
|
+
|
299
|
+
test 'broken associations should not cause errors' do
|
300
|
+
class Author < MockModel; end
|
301
|
+
class Book < MockModel
|
302
|
+
belongs_to :author
|
303
|
+
belongs_to :invalid
|
304
|
+
end
|
305
|
+
|
306
|
+
assert_equal(
|
307
|
+
[foreign_key_definition(
|
308
|
+
'books', 'authors',
|
309
|
+
:column => 'author_id', :primary_key => 'id', :dependent => nil
|
310
|
+
)],
|
311
|
+
Immigrant.infer_keys([], [Author, Book]).first
|
312
|
+
)
|
313
|
+
end
|
314
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: immigrant
|
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
|
+
- Jon Jensen
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-04-01 00:00:00 -06:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: activerecord
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 7
|
30
|
+
segments:
|
31
|
+
- 3
|
32
|
+
- 0
|
33
|
+
version: "3.0"
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: foreigner
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 21
|
45
|
+
segments:
|
46
|
+
- 1
|
47
|
+
- 1
|
48
|
+
- 3
|
49
|
+
version: 1.1.3
|
50
|
+
type: :runtime
|
51
|
+
version_requirements: *id002
|
52
|
+
description: Adds a generator for creating a foreign key migration based on your current model associations
|
53
|
+
email: jenseng@gmail.com
|
54
|
+
executables: []
|
55
|
+
|
56
|
+
extensions: []
|
57
|
+
|
58
|
+
extra_rdoc_files:
|
59
|
+
- README.rdoc
|
60
|
+
files:
|
61
|
+
- LICENSE.txt
|
62
|
+
- Rakefile
|
63
|
+
- README.rdoc
|
64
|
+
- lib/generators/USAGE
|
65
|
+
- lib/generators/immigration_generator.rb
|
66
|
+
- lib/generators/templates/immigration-pre-3.1.rb
|
67
|
+
- lib/generators/templates/immigration.rb
|
68
|
+
- lib/immigrant/foreign_key_definition.rb
|
69
|
+
- lib/immigrant/loader.rb
|
70
|
+
- lib/immigrant/railtie.rb
|
71
|
+
- lib/immigrant.rb
|
72
|
+
- test/helper.rb
|
73
|
+
- test/immigrant_test.rb
|
74
|
+
has_rdoc: true
|
75
|
+
homepage: http://github.com/jenseng/immigrant
|
76
|
+
licenses: []
|
77
|
+
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
hash: 57
|
89
|
+
segments:
|
90
|
+
- 1
|
91
|
+
- 8
|
92
|
+
- 7
|
93
|
+
version: 1.8.7
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
hash: 17
|
100
|
+
segments:
|
101
|
+
- 1
|
102
|
+
- 3
|
103
|
+
- 5
|
104
|
+
version: 1.3.5
|
105
|
+
requirements: []
|
106
|
+
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 1.6.2
|
109
|
+
signing_key:
|
110
|
+
specification_version: 3
|
111
|
+
summary: Migration generator for Foreigner
|
112
|
+
test_files: []
|
113
|
+
|