immigrant 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|