ws-foreigner 1.2.2
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/MIT-LICENSE +20 -0
- data/README.rdoc +70 -0
- data/Rakefile +18 -0
- data/lib/foreigner.rb +28 -0
- data/lib/foreigner/adapter.rb +22 -0
- data/lib/foreigner/connection_adapters/abstract/schema_definitions.rb +70 -0
- data/lib/foreigner/connection_adapters/abstract/schema_statements.rb +77 -0
- data/lib/foreigner/connection_adapters/mysql2_adapter.rb +54 -0
- data/lib/foreigner/connection_adapters/mysql_adapter.rb +1 -0
- data/lib/foreigner/connection_adapters/postgresql_adapter.rb +44 -0
- data/lib/foreigner/connection_adapters/sql2003.rb +66 -0
- data/lib/foreigner/migration/command_recorder.rb +28 -0
- data/lib/foreigner/railtie.rb +24 -0
- data/lib/foreigner/schema_dumper.rb +50 -0
- data/test/helper.rb +43 -0
- data/test/unit/command_recorder_test.rb +40 -0
- data/test/unit/mysql2_adapter_test.rb +27 -0
- data/test/unit/mysql_adapter_test.rb +10 -0
- data/test/unit/postgresql_adapter_test.rb +6 -0
- data/test/unit/schema_dumper_test.rb +44 -0
- data/test/unit/sql2003_test.rb +86 -0
- metadata +90 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 [name of plugin creator]
|
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,70 @@
|
|
1
|
+
= Foreigner
|
2
|
+
|
3
|
+
Foreigner introduces a few methods to your migrations for adding and removing foreign key constraints. It also dumps foreign keys to schema.rb.
|
4
|
+
|
5
|
+
The following adapters are supported:
|
6
|
+
|
7
|
+
* mysql2
|
8
|
+
* postgres
|
9
|
+
* sqlite (foreign key methods are a no-op)
|
10
|
+
|
11
|
+
== Installation
|
12
|
+
|
13
|
+
Add the following to your Gemfile:
|
14
|
+
|
15
|
+
gem 'foreigner'
|
16
|
+
|
17
|
+
== API Examples
|
18
|
+
|
19
|
+
Foreigner adds two methods to migrations.
|
20
|
+
|
21
|
+
* add_foreign_key(from_table, to_table, options)
|
22
|
+
* remove_foreign_key(from_table, options)
|
23
|
+
|
24
|
+
(Options are documented in connection_adapters/abstract/schema_definitions.rb):
|
25
|
+
|
26
|
+
For example, given the following model:
|
27
|
+
|
28
|
+
class Comment < ActiveRecord::Base
|
29
|
+
belongs_to :post
|
30
|
+
end
|
31
|
+
|
32
|
+
class Post < ActiveRecord::Base
|
33
|
+
has_many :comments, :dependent => :delete_all
|
34
|
+
end
|
35
|
+
|
36
|
+
You should add a foreign key in your migration:
|
37
|
+
|
38
|
+
add_foreign_key(:comments, :posts)
|
39
|
+
|
40
|
+
The :dependent option can be moved from the has_many definition to the foreign key:
|
41
|
+
|
42
|
+
add_foreign_key(:comments, :posts, :dependent => :delete)
|
43
|
+
|
44
|
+
If the column is named article_id instead of post_id, use the :column option:
|
45
|
+
|
46
|
+
add_foreign_key(:comments, :posts, :column => 'article_id')
|
47
|
+
|
48
|
+
A name can be specified for the foreign key constraint:
|
49
|
+
|
50
|
+
add_foreign_key(:comments, :posts, :name => 'comment_article_foreign_key')
|
51
|
+
|
52
|
+
== Change Table Methods
|
53
|
+
|
54
|
+
Foreigner adds extra methods to change_table.
|
55
|
+
|
56
|
+
Add a missing foreign key to comments:
|
57
|
+
|
58
|
+
change_table :comments do |t|
|
59
|
+
t.foreign_key :posts, :dependent => :delete
|
60
|
+
end
|
61
|
+
|
62
|
+
Remove an unwanted foreign key:
|
63
|
+
|
64
|
+
change_table :comments do |t|
|
65
|
+
t.remove_foreign_key :users
|
66
|
+
end
|
67
|
+
|
68
|
+
== License
|
69
|
+
|
70
|
+
Copyright (c) 2011 Matthew Higgins, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rake'
|
2
|
+
# begin
|
3
|
+
# require 'bundler/setup'
|
4
|
+
# rescue LoadError
|
5
|
+
# puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
# end
|
7
|
+
|
8
|
+
desc 'Default: run unit tests.'
|
9
|
+
task :default => :test
|
10
|
+
|
11
|
+
require 'rake/testtask'
|
12
|
+
desc 'Test the foreigner plugin.'
|
13
|
+
Rake::TestTask.new(:test) do |t|
|
14
|
+
t.libs << 'lib'
|
15
|
+
t.libs << 'test'
|
16
|
+
t.pattern = 'test/**/*_test.rb'
|
17
|
+
t.verbose = true
|
18
|
+
end
|
data/lib/foreigner.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
|
3
|
+
module Foreigner
|
4
|
+
extend ActiveSupport::Autoload
|
5
|
+
autoload :Adapter
|
6
|
+
autoload :SchemaDumper
|
7
|
+
|
8
|
+
module ConnectionAdapters
|
9
|
+
extend ActiveSupport::Autoload
|
10
|
+
autoload :Sql2003
|
11
|
+
|
12
|
+
autoload_under 'abstract' do
|
13
|
+
autoload :SchemaDefinitions
|
14
|
+
autoload :SchemaStatements
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Migration
|
19
|
+
autoload :CommandRecorder, 'foreigner/migration/command_recorder'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Foreigner::Adapter.register 'mysql', 'foreigner/connection_adapters/mysql_adapter'
|
24
|
+
Foreigner::Adapter.register 'mysql2', 'foreigner/connection_adapters/mysql2_adapter'
|
25
|
+
Foreigner::Adapter.register 'jdbcmysql', 'foreigner/connection_adapters/mysql2_adapter'
|
26
|
+
Foreigner::Adapter.register 'postgresql', 'foreigner/connection_adapters/postgresql_adapter'
|
27
|
+
|
28
|
+
require 'foreigner/railtie' if defined?(Rails)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Foreigner
|
2
|
+
class Adapter
|
3
|
+
class_attribute :registered
|
4
|
+
self.registered = {}
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def register(adapter_name, file_name)
|
8
|
+
registered[adapter_name] = file_name
|
9
|
+
end
|
10
|
+
|
11
|
+
def load!
|
12
|
+
if registered.key?(configured_name)
|
13
|
+
require registered[configured_name]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def configured_name
|
18
|
+
ActiveRecord::Base.connection_pool.spec.config[:adapter]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Foreigner
|
2
|
+
module ConnectionAdapters
|
3
|
+
class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
|
4
|
+
end
|
5
|
+
|
6
|
+
module SchemaDefinitions
|
7
|
+
def self.included(base)
|
8
|
+
base::Table.class_eval do
|
9
|
+
include Foreigner::ConnectionAdapters::Table
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Table
|
15
|
+
extend ActiveSupport::Concern
|
16
|
+
|
17
|
+
included do
|
18
|
+
alias_method_chain :references, :foreign_keys
|
19
|
+
end
|
20
|
+
|
21
|
+
# Adds a new foreign key to the table. +to_table+ can be a single Symbol, or
|
22
|
+
# an Array of Symbols. See SchemaStatements#add_foreign_key
|
23
|
+
#
|
24
|
+
# ===== Examples
|
25
|
+
# ====== Creating a simple foreign key
|
26
|
+
# t.foreign_key(:people)
|
27
|
+
# ====== Defining the column
|
28
|
+
# t.foreign_key(:people, :column => :sender_id)
|
29
|
+
# ====== Creating a named foreign key
|
30
|
+
# t.foreign_key(:people, :column => :sender_id, :name => 'sender_foreign_key')
|
31
|
+
# ====== Defining the column of the +to_table+.
|
32
|
+
# t.foreign_key(:people, :column => :sender_id, :primary_key => :person_id)
|
33
|
+
def foreign_key(to_table, options = {})
|
34
|
+
@base.add_foreign_key(@table_name, to_table, options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Remove the given foreign key from the table.
|
38
|
+
#
|
39
|
+
# ===== Examples
|
40
|
+
# ====== Remove the suppliers_company_id_fk in the suppliers table.
|
41
|
+
# change_table :suppliers do |t|
|
42
|
+
# t.remove_foreign_key :companies
|
43
|
+
# end
|
44
|
+
# ====== Remove the foreign key named accounts_branch_id_fk in the accounts table.
|
45
|
+
# change_table :accounts do |t|
|
46
|
+
# t.remove_foreign_key :column => :branch_id
|
47
|
+
# end
|
48
|
+
# ====== Remove the foreign key named party_foreign_key in the accounts table.
|
49
|
+
# change_table :accounts do |t|
|
50
|
+
# t.remove_index :name => :party_foreign_key
|
51
|
+
# end
|
52
|
+
def remove_foreign_key(options)
|
53
|
+
@base.remove_foreign_key(@table_name, options)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Deprecated
|
57
|
+
def references_with_foreign_keys(*args)
|
58
|
+
options = args.extract_options!
|
59
|
+
|
60
|
+
if fk_options = options.delete(:foreign_key)
|
61
|
+
p ActiveSupport::Deprecation.send(:deprecation_message, caller,
|
62
|
+
":foreign_key in t.references is deprecated. " \
|
63
|
+
"Use t.foreign_key instead")
|
64
|
+
end
|
65
|
+
|
66
|
+
references_without_foreign_keys(*(args.dup << options))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Foreigner
|
2
|
+
module ConnectionAdapters
|
3
|
+
module SchemaStatements
|
4
|
+
def self.included(base)
|
5
|
+
base::AbstractAdapter.class_eval do
|
6
|
+
include Foreigner::ConnectionAdapters::AbstractAdapter
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module AbstractAdapter
|
12
|
+
def supports_foreign_keys?
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
# Adds a new foreign key to the +from_table+, referencing the primary key of +to_table+
|
17
|
+
#
|
18
|
+
# The foreign key will be named after the from and to tables unless you pass
|
19
|
+
# <tt>:name</tt> as an option.
|
20
|
+
#
|
21
|
+
# ===== Examples
|
22
|
+
# ====== Creating a foreign key
|
23
|
+
# add_foreign_key(:comments, :posts)
|
24
|
+
# generates
|
25
|
+
# ALTER TABLE `comments` ADD CONSTRAINT
|
26
|
+
# `comments_post_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`)
|
27
|
+
#
|
28
|
+
# ====== Creating a named foreign key
|
29
|
+
# add_foreign_key(:comments, :posts, :name => 'comments_belongs_to_posts')
|
30
|
+
# generates
|
31
|
+
# ALTER TABLE `comments` ADD CONSTRAINT
|
32
|
+
# `comments_belongs_to_posts` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`)
|
33
|
+
#
|
34
|
+
# ====== Creating a cascading foreign_key on a custom column
|
35
|
+
# add_foreign_key(:people, :people, :column => 'best_friend_id', :dependent => :nullify)
|
36
|
+
# generates
|
37
|
+
# ALTER TABLE `people` ADD CONSTRAINT
|
38
|
+
# `people_best_friend_id_fk` FOREIGN KEY (`best_friend_id`) REFERENCES `people` (`id`)
|
39
|
+
# ON DELETE SET NULL
|
40
|
+
#
|
41
|
+
# === Supported options
|
42
|
+
# [:column]
|
43
|
+
# Specify the column name on the from_table that references the to_table. By default this is guessed
|
44
|
+
# to be the singular name of the to_table with "_id" suffixed. So a to_table of :posts will use "post_id"
|
45
|
+
# as the default <tt>:column</tt>.
|
46
|
+
# [:primary_key]
|
47
|
+
# Specify the column name on the to_table that is referenced by this foreign key. By default this is
|
48
|
+
# assumed to be "id".
|
49
|
+
# [:name]
|
50
|
+
# Specify the name of the foreign key constraint. This defaults to use from_table and foreign key column.
|
51
|
+
# [:dependent]
|
52
|
+
# If set to <tt>:delete</tt>, the associated records in from_table are deleted when records in to_table table are deleted.
|
53
|
+
# If set to <tt>:nullify</tt>, the foreign key column is set to +NULL+.
|
54
|
+
# [:options]
|
55
|
+
# Any extra options you want appended to the foreign key definition.
|
56
|
+
def add_foreign_key(from_table, to_table, options = {})
|
57
|
+
end
|
58
|
+
|
59
|
+
# Remove the given foreign key from the table.
|
60
|
+
#
|
61
|
+
# ===== Examples
|
62
|
+
# ====== Remove the suppliers_company_id_fk in the suppliers table.
|
63
|
+
# remove_foreign_key :suppliers, :companies
|
64
|
+
# ====== Remove the foreign key named accounts_branch_id_fk in the accounts table.
|
65
|
+
# remove_foreign_key :accounts, :column => :branch_id
|
66
|
+
# ====== Remove the foreign key named party_foreign_key in the accounts table.
|
67
|
+
# remove_foreign_key :accounts, :name => :party_foreign_key
|
68
|
+
def remove_foreign_key(from_table, options)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return the foreign keys for the schema_dumper
|
72
|
+
def foreign_keys(table_name)
|
73
|
+
[]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Foreigner
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Mysql2Adapter
|
4
|
+
include Foreigner::ConnectionAdapters::Sql2003
|
5
|
+
|
6
|
+
def remove_foreign_key_sql(table, options)
|
7
|
+
if Hash === options
|
8
|
+
foreign_key_name = foreign_key_name(table, options[:column], options)
|
9
|
+
else
|
10
|
+
foreign_key_name = foreign_key_name(table, "#{options.to_s.singularize}_id")
|
11
|
+
end
|
12
|
+
|
13
|
+
"DROP FOREIGN KEY #{quote_column_name(foreign_key_name)}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def foreign_keys(table_name)
|
17
|
+
fk_info = select_all %{
|
18
|
+
SELECT fk.referenced_table_name as 'to_table'
|
19
|
+
,fk.referenced_column_name as 'primary_key'
|
20
|
+
,fk.column_name as 'column'
|
21
|
+
,fk.constraint_name as 'name'
|
22
|
+
FROM information_schema.key_column_usage fk
|
23
|
+
WHERE fk.referenced_column_name is not null
|
24
|
+
AND fk.table_schema = '#{@config[:database]}'
|
25
|
+
AND fk.table_name = '#{table_name}'
|
26
|
+
}
|
27
|
+
|
28
|
+
create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
|
29
|
+
|
30
|
+
fk_info.map do |row|
|
31
|
+
options = {:column => row['column'], :name => row['name'], :primary_key => row['primary_key']}
|
32
|
+
|
33
|
+
if create_table_info =~ /CONSTRAINT #{quote_column_name(row['name'])} FOREIGN KEY .* REFERENCES .* ON DELETE (CASCADE|SET NULL|RESTRICT)/
|
34
|
+
options[:dependent] = case $1
|
35
|
+
when 'CASCADE' then :delete
|
36
|
+
when 'SET NULL' then :nullify
|
37
|
+
when 'RESTRICT' then :restrict
|
38
|
+
end
|
39
|
+
end
|
40
|
+
ForeignKeyDefinition.new(table_name, row['to_table'], options)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
[:Mysql2Adapter, :JdbcAdapter].each do |adapter|
|
48
|
+
begin
|
49
|
+
ActiveRecord::ConnectionAdapters.const_get(adapter).class_eval do
|
50
|
+
include Foreigner::ConnectionAdapters::Mysql2Adapter
|
51
|
+
end
|
52
|
+
rescue
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
p "WARNING: Please upgrade to mysql2. The old mysql adapter is not supported by foreigner."
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Foreigner
|
2
|
+
module ConnectionAdapters
|
3
|
+
module PostgreSQLAdapter
|
4
|
+
include Foreigner::ConnectionAdapters::Sql2003
|
5
|
+
|
6
|
+
def foreign_keys(table_name)
|
7
|
+
fk_info = select_all %{
|
8
|
+
SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confdeltype AS dependency
|
9
|
+
FROM pg_constraint c
|
10
|
+
JOIN pg_class t1 ON c.conrelid = t1.oid
|
11
|
+
JOIN pg_class t2 ON c.confrelid = t2.oid
|
12
|
+
JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
|
13
|
+
JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
|
14
|
+
JOIN pg_namespace t3 ON c.connamespace = t3.oid
|
15
|
+
WHERE c.contype = 'f'
|
16
|
+
AND t1.relname = '#{table_name}'
|
17
|
+
AND t3.nspname = ANY (current_schemas(false))
|
18
|
+
ORDER BY c.conname
|
19
|
+
}
|
20
|
+
|
21
|
+
fk_info.map do |row|
|
22
|
+
options = {:column => row['column'], :name => row['name'], :primary_key => row['primary_key']}
|
23
|
+
|
24
|
+
options[:dependent] = case row['dependency']
|
25
|
+
when 'c' then :delete
|
26
|
+
when 'n' then :nullify
|
27
|
+
when 'r' then :restrict
|
28
|
+
end
|
29
|
+
|
30
|
+
ForeignKeyDefinition.new(table_name, row['to_table'], options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
[:PostgreSQLAdapter, :JdbcAdapter].each do |adapter|
|
38
|
+
begin
|
39
|
+
ActiveRecord::ConnectionAdapters.const_get(adapter).class_eval do
|
40
|
+
include Foreigner::ConnectionAdapters::PostgreSQLAdapter
|
41
|
+
end
|
42
|
+
rescue
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Foreigner
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Sql2003
|
4
|
+
def supports_foreign_keys?
|
5
|
+
true
|
6
|
+
end
|
7
|
+
|
8
|
+
def drop_table(*args)
|
9
|
+
disable_referential_integrity { super }
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_foreign_key(from_table, to_table, options = {})
|
13
|
+
sql = "ALTER TABLE #{quote_table_name(from_table)} #{add_foreign_key_sql(from_table, to_table, options)}"
|
14
|
+
execute(sql)
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_foreign_key_sql(from_table, to_table, options = {})
|
18
|
+
column = options[:column] || "#{to_table.to_s.singularize}_id"
|
19
|
+
foreign_key_name = foreign_key_name(from_table, column, options)
|
20
|
+
primary_key = options[:primary_key] || "id"
|
21
|
+
dependency = dependency_sql(options[:dependent])
|
22
|
+
|
23
|
+
sql =
|
24
|
+
"ADD CONSTRAINT #{quote_column_name(foreign_key_name)} " +
|
25
|
+
"FOREIGN KEY (#{quote_column_name(column)}) " +
|
26
|
+
"REFERENCES #{quote_table_name(ActiveRecord::Migrator.proper_table_name(to_table))}(#{primary_key})"
|
27
|
+
sql << " #{dependency}" if dependency.present?
|
28
|
+
sql << " #{options[:options]}" if options[:options]
|
29
|
+
|
30
|
+
sql
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove_foreign_key(table, options)
|
34
|
+
execute "ALTER TABLE #{quote_table_name(table)} #{remove_foreign_key_sql(table, options)}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def remove_foreign_key_sql(table, options)
|
38
|
+
if Hash === options
|
39
|
+
foreign_key_name = foreign_key_name(table, options[:column], options)
|
40
|
+
else
|
41
|
+
foreign_key_name = foreign_key_name(table, "#{options.to_s.singularize}_id")
|
42
|
+
end
|
43
|
+
|
44
|
+
"DROP CONSTRAINT #{quote_column_name(foreign_key_name)}"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def foreign_key_name(table, column, options = {})
|
49
|
+
if options[:name]
|
50
|
+
options[:name]
|
51
|
+
else
|
52
|
+
"#{table}_#{column}_fk"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def dependency_sql(dependency)
|
57
|
+
case dependency
|
58
|
+
when :nullify then "ON DELETE SET NULL"
|
59
|
+
when :delete then "ON DELETE CASCADE"
|
60
|
+
when :restrict then "ON DELETE RESTRICT"
|
61
|
+
else ""
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Foreigner
|
2
|
+
module Migration
|
3
|
+
module CommandRecorder
|
4
|
+
def add_foreign_key(*args)
|
5
|
+
record(:add_foreign_key, args)
|
6
|
+
end
|
7
|
+
|
8
|
+
def remove_foreign_key(*args)
|
9
|
+
record(:remove_foreign_key, args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def invert_add_foreign_key(args)
|
13
|
+
from_table, to_table, add_options = *args
|
14
|
+
add_options ||= {}
|
15
|
+
|
16
|
+
if add_options[:name]
|
17
|
+
options = {:name => add_options[:name]}
|
18
|
+
elsif add_options[:column]
|
19
|
+
options = {:column => add_options[:column]}
|
20
|
+
else
|
21
|
+
options = to_table
|
22
|
+
end
|
23
|
+
|
24
|
+
[:remove_foreign_key, [from_table, options]]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Foreigner
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
initializer 'foreigner.load_adapter' do
|
4
|
+
ActiveSupport.on_load :active_record do
|
5
|
+
ActiveRecord::ConnectionAdapters.module_eval do
|
6
|
+
include Foreigner::ConnectionAdapters::SchemaStatements
|
7
|
+
include Foreigner::ConnectionAdapters::SchemaDefinitions
|
8
|
+
end
|
9
|
+
|
10
|
+
ActiveRecord::SchemaDumper.class_eval do
|
11
|
+
include Foreigner::SchemaDumper
|
12
|
+
end
|
13
|
+
|
14
|
+
if defined?(ActiveRecord::Migration::CommandRecorder)
|
15
|
+
ActiveRecord::Migration::CommandRecorder.class_eval do
|
16
|
+
include Foreigner::Migration::CommandRecorder
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Foreigner::Adapter.load!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Foreigner
|
2
|
+
module SchemaDumper
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
alias_method_chain :tables, :foreign_keys
|
7
|
+
end
|
8
|
+
|
9
|
+
def tables_with_foreign_keys(stream)
|
10
|
+
tables_without_foreign_keys(stream)
|
11
|
+
@connection.tables.sort.each do |table|
|
12
|
+
next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
|
13
|
+
case ignored
|
14
|
+
when String; table == ignored
|
15
|
+
when Regexp; table =~ ignored
|
16
|
+
else
|
17
|
+
raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
foreign_keys(table, stream)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def foreign_keys(table_name, stream)
|
26
|
+
if (foreign_keys = @connection.foreign_keys(table_name)).any?
|
27
|
+
add_foreign_key_statements = foreign_keys.map do |foreign_key|
|
28
|
+
statement_parts = [ ('add_foreign_key ' + foreign_key.from_table.inspect) ]
|
29
|
+
statement_parts << foreign_key.to_table.inspect
|
30
|
+
statement_parts << (':name => ' + foreign_key.options[:name].inspect)
|
31
|
+
|
32
|
+
if foreign_key.options[:column] != "#{foreign_key.to_table.singularize}_id"
|
33
|
+
statement_parts << (':column => ' + foreign_key.options[:column].inspect)
|
34
|
+
end
|
35
|
+
if foreign_key.options[:primary_key] != 'id'
|
36
|
+
statement_parts << (':primary_key => ' + foreign_key.options[:primary_key].inspect)
|
37
|
+
end
|
38
|
+
if foreign_key.options[:dependent].present?
|
39
|
+
statement_parts << (':dependent => ' + foreign_key.options[:dependent].inspect)
|
40
|
+
end
|
41
|
+
|
42
|
+
' ' + statement_parts.join(', ')
|
43
|
+
end
|
44
|
+
|
45
|
+
stream.puts add_foreign_key_statements.sort.join("\n")
|
46
|
+
stream.puts
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
require File.expand_path('../../lib/foreigner', __FILE__)
|
6
|
+
|
7
|
+
# Foreigner::Adapter.registered.values.each do |file_name|
|
8
|
+
# require file_name
|
9
|
+
# end
|
10
|
+
|
11
|
+
module Foreigner
|
12
|
+
class UnitTest < ActiveSupport::TestCase
|
13
|
+
private
|
14
|
+
def execute(sql, name = nil)
|
15
|
+
sql_statements << sql
|
16
|
+
sql
|
17
|
+
end
|
18
|
+
|
19
|
+
def quote_column_name(name)
|
20
|
+
"`#{name}`"
|
21
|
+
end
|
22
|
+
|
23
|
+
def quote_table_name(name)
|
24
|
+
quote_column_name(name).gsub('.', '`.`')
|
25
|
+
end
|
26
|
+
|
27
|
+
def sql_statements
|
28
|
+
@sql_statements ||= []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class IntegrationTest < ActiveSupport::TestCase
|
33
|
+
def with_migration(&blk)
|
34
|
+
migration = Class.new(ActiveRecord::Migration)
|
35
|
+
|
36
|
+
migration.singleton_class do
|
37
|
+
define_method(:up, &blk)
|
38
|
+
end
|
39
|
+
|
40
|
+
migration
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
ActiveRecord::Migration::CommandRecorder.class_eval do
|
4
|
+
include ::Foreigner::Migration::CommandRecorder
|
5
|
+
end
|
6
|
+
|
7
|
+
class Foreigner::CommandRecorderTest < Foreigner::UnitTest
|
8
|
+
def setup
|
9
|
+
@recorder = ActiveRecord::Migration::CommandRecorder.new
|
10
|
+
end
|
11
|
+
|
12
|
+
test 'invert_add_foreign_key' do
|
13
|
+
@recorder.add_foreign_key(:employees, :companies)
|
14
|
+
remove = @recorder.inverse.first
|
15
|
+
assert_equal [:remove_foreign_key, [:employees, :companies]], remove
|
16
|
+
end
|
17
|
+
|
18
|
+
test 'invert_add_foreign_key with column' do
|
19
|
+
@recorder.add_foreign_key(:employees, :companies, :column => :place_id)
|
20
|
+
remove = @recorder.inverse.first
|
21
|
+
assert_equal [:remove_foreign_key, [:employees, {:column => :place_id}]], remove
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'invert_add_foreign_key with name' do
|
25
|
+
@recorder.add_foreign_key(:employees, :companies, :name => 'the_best_fk', :column => :place_id)
|
26
|
+
remove = @recorder.inverse.first
|
27
|
+
assert_equal [:remove_foreign_key, [:employees, {:name => 'the_best_fk'}]], remove
|
28
|
+
|
29
|
+
@recorder.record :rename_table, [:old, :new]
|
30
|
+
rename = @recorder.inverse.first
|
31
|
+
assert_equal [:rename_table, [:new, :old]], rename
|
32
|
+
end
|
33
|
+
|
34
|
+
test 'remove_foreign_key is irreversible' do
|
35
|
+
@recorder.remove_foreign_key(:employees, :companies)
|
36
|
+
assert_raise ActiveRecord::IrreversibleMigration do
|
37
|
+
@recorder.inverse
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'foreigner/connection_adapters/mysql2_adapter'
|
3
|
+
|
4
|
+
class Foreigner::Mysql2AdapterTest < Foreigner::UnitTest
|
5
|
+
include Foreigner::ConnectionAdapters::Mysql2Adapter
|
6
|
+
|
7
|
+
test 'remove_foreign_key_sql by table' do
|
8
|
+
assert_equal(
|
9
|
+
"DROP FOREIGN KEY `suppliers_company_id_fk`",
|
10
|
+
remove_foreign_key_sql(:suppliers, :companies)
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
test 'remove_foreign_key_sql by name' do
|
15
|
+
assert_equal(
|
16
|
+
"DROP FOREIGN KEY `belongs_to_supplier`",
|
17
|
+
remove_foreign_key_sql(:suppliers, :name => "belongs_to_supplier")
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
test 'remove_foreign_key_sql by column' do
|
22
|
+
assert_equal(
|
23
|
+
"DROP FOREIGN KEY `suppliers_ship_to_id_fk`",
|
24
|
+
remove_foreign_key_sql(:suppliers, :column => "ship_to_id")
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class Foreigner::MysqlAdapterTest < Foreigner::UnitTest
|
4
|
+
test 'warning' do
|
5
|
+
output = capture(:stdout) do
|
6
|
+
require 'foreigner/connection_adapters/mysql_adapter'
|
7
|
+
end
|
8
|
+
assert_match /WARNING: Please upgrade to mysql2. The old mysql adapter is not supported by foreigner./, output
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class Foreigner::SchemaDumperTest < Foreigner::UnitTest
|
4
|
+
|
5
|
+
class MockConnection
|
6
|
+
def tables
|
7
|
+
[ 'foo', 'bar' ]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class MockSchemaDumper
|
12
|
+
cattr_accessor :ignore_tables, :processed_tables
|
13
|
+
@@ignore_table = []
|
14
|
+
@@processed_tables = []
|
15
|
+
|
16
|
+
@connection = MockConnection.new
|
17
|
+
|
18
|
+
# need this here so ActiveRecord::Concern has something to redefine
|
19
|
+
def tables
|
20
|
+
end
|
21
|
+
|
22
|
+
include Foreigner::SchemaDumper
|
23
|
+
|
24
|
+
# override this method so we don't have to mock up
|
25
|
+
# all of the necessary scafolding for things to work
|
26
|
+
def foreign_keys(table, stream)
|
27
|
+
processed_tables << table
|
28
|
+
end
|
29
|
+
|
30
|
+
def tables(ignore_list)
|
31
|
+
ignore_tables = ignore_list
|
32
|
+
processed_table = nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
test 'returns_all_tables' do
|
37
|
+
assert MockSchemaDumper.new(nil).processed_tables.sort.to_s, "['bar', 'foo']"
|
38
|
+
end
|
39
|
+
|
40
|
+
test 'only_returns_1_table' do
|
41
|
+
assert MockSchemaDumper.new('foo').processed_tables.to_s, "['bar']"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class Foreigner::Sql2003Test < Foreigner::UnitTest
|
4
|
+
include Foreigner::ConnectionAdapters::Sql2003
|
5
|
+
|
6
|
+
test 'add_without_options' do
|
7
|
+
assert_equal(
|
8
|
+
"ALTER TABLE `employees` ADD CONSTRAINT `employees_company_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id)",
|
9
|
+
add_foreign_key(:employees, :companies)
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
test 'add_with_name' do
|
14
|
+
assert_equal(
|
15
|
+
"ALTER TABLE `employees` ADD CONSTRAINT `favorite_company_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id)",
|
16
|
+
add_foreign_key(:employees, :companies, :name => 'favorite_company_fk')
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
test 'add_with_column' do
|
21
|
+
assert_equal(
|
22
|
+
"ALTER TABLE `employees` ADD CONSTRAINT `employees_last_employer_id_fk` FOREIGN KEY (`last_employer_id`) REFERENCES `companies`(id)",
|
23
|
+
add_foreign_key(:employees, :companies, :column => 'last_employer_id')
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
test 'add_with_column_and_name' do
|
28
|
+
assert_equal(
|
29
|
+
"ALTER TABLE `employees` ADD CONSTRAINT `favorite_company_fk` FOREIGN KEY (`last_employer_id`) REFERENCES `companies`(id)",
|
30
|
+
add_foreign_key(:employees, :companies, :column => 'last_employer_id', :name => 'favorite_company_fk')
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
test 'add_with_delete_dependency' do
|
35
|
+
assert_equal(
|
36
|
+
"ALTER TABLE `employees` ADD CONSTRAINT `employees_company_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id) " +
|
37
|
+
"ON DELETE CASCADE",
|
38
|
+
add_foreign_key(:employees, :companies, :dependent => :delete)
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'add_with_nullify_dependency' do
|
43
|
+
assert_equal(
|
44
|
+
"ALTER TABLE `employees` ADD CONSTRAINT `employees_company_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id) " +
|
45
|
+
"ON DELETE SET NULL",
|
46
|
+
add_foreign_key(:employees, :companies, :dependent => :nullify)
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
test 'add_with_restrict_dependency' do
|
51
|
+
assert_equal(
|
52
|
+
"ALTER TABLE `employees` ADD CONSTRAINT `employees_company_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id) " +
|
53
|
+
"ON DELETE RESTRICT",
|
54
|
+
add_foreign_key(:employees, :companies, :dependent => :restrict)
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
test 'add_with_options' do
|
59
|
+
assert_equal(
|
60
|
+
"ALTER TABLE `employees` ADD CONSTRAINT `employees_company_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id) " +
|
61
|
+
"on delete foo",
|
62
|
+
add_foreign_key(:employees, :companies, :options => 'on delete foo')
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
test 'remove_by_table' do
|
67
|
+
assert_equal(
|
68
|
+
"ALTER TABLE `suppliers` DROP CONSTRAINT `suppliers_company_id_fk`",
|
69
|
+
remove_foreign_key(:suppliers, :companies)
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
test 'remove_by_name' do
|
74
|
+
assert_equal(
|
75
|
+
"ALTER TABLE `suppliers` DROP CONSTRAINT `belongs_to_supplier`",
|
76
|
+
remove_foreign_key(:suppliers, :name => "belongs_to_supplier")
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
test 'remove_by_column' do
|
81
|
+
assert_equal(
|
82
|
+
"ALTER TABLE `suppliers` DROP CONSTRAINT `suppliers_ship_to_id_fk`",
|
83
|
+
remove_foreign_key(:suppliers, :column => "ship_to_id")
|
84
|
+
)
|
85
|
+
end
|
86
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ws-foreigner
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.2.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- ! 'Matthew Higgins // fork: polo ornelas'
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-05 00:00:00.000000000 -06:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activerecord
|
17
|
+
requirement: &70162592211660 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.1.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70162592211660
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: activerecord
|
28
|
+
requirement: &70162592211200 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.1.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *70162592211200
|
37
|
+
description: Adds helpers to migrations and dumps foreign keys to schema.rb
|
38
|
+
email: ! 'developer@matthewhiggins.com // fork: poloornelas@yahoo.com'
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README.rdoc
|
43
|
+
files:
|
44
|
+
- MIT-LICENSE
|
45
|
+
- Rakefile
|
46
|
+
- README.rdoc
|
47
|
+
- lib/foreigner/adapter.rb
|
48
|
+
- lib/foreigner/connection_adapters/abstract/schema_definitions.rb
|
49
|
+
- lib/foreigner/connection_adapters/abstract/schema_statements.rb
|
50
|
+
- lib/foreigner/connection_adapters/mysql2_adapter.rb
|
51
|
+
- lib/foreigner/connection_adapters/mysql_adapter.rb
|
52
|
+
- lib/foreigner/connection_adapters/postgresql_adapter.rb
|
53
|
+
- lib/foreigner/connection_adapters/sql2003.rb
|
54
|
+
- lib/foreigner/migration/command_recorder.rb
|
55
|
+
- lib/foreigner/railtie.rb
|
56
|
+
- lib/foreigner/schema_dumper.rb
|
57
|
+
- lib/foreigner.rb
|
58
|
+
- test/helper.rb
|
59
|
+
- test/unit/command_recorder_test.rb
|
60
|
+
- test/unit/mysql2_adapter_test.rb
|
61
|
+
- test/unit/mysql_adapter_test.rb
|
62
|
+
- test/unit/postgresql_adapter_test.rb
|
63
|
+
- test/unit/schema_dumper_test.rb
|
64
|
+
- test/unit/sql2003_test.rb
|
65
|
+
has_rdoc: true
|
66
|
+
homepage: https://github.com/warstories/foreigner
|
67
|
+
licenses: []
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.8.7
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 1.3.5
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project: foreigner
|
86
|
+
rubygems_version: 1.6.2
|
87
|
+
signing_key:
|
88
|
+
specification_version: 3
|
89
|
+
summary: Foreign Keys for Rails, forked to work with rails 3.1
|
90
|
+
test_files: []
|