schema_plus_foreign_keys 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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +21 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +200 -0
- data/Rakefile +9 -0
- data/gemfiles/Gemfile.base +4 -0
- data/gemfiles/activerecord-4.2.0/Gemfile.base +3 -0
- data/gemfiles/activerecord-4.2.0/Gemfile.mysql2 +10 -0
- data/gemfiles/activerecord-4.2.0/Gemfile.postgresql +10 -0
- data/gemfiles/activerecord-4.2.0/Gemfile.sqlite3 +10 -0
- data/gemfiles/activerecord-4.2.1/Gemfile.base +3 -0
- data/gemfiles/activerecord-4.2.1/Gemfile.mysql2 +10 -0
- data/gemfiles/activerecord-4.2.1/Gemfile.postgresql +10 -0
- data/gemfiles/activerecord-4.2.1/Gemfile.sqlite3 +10 -0
- data/lib/schema_plus/foreign_keys.rb +78 -0
- data/lib/schema_plus/foreign_keys/active_record/base.rb +33 -0
- data/lib/schema_plus/foreign_keys/active_record/connection_adapters/abstract_adapter.rb +168 -0
- data/lib/schema_plus/foreign_keys/active_record/connection_adapters/foreign_key_definition.rb +137 -0
- data/lib/schema_plus/foreign_keys/active_record/connection_adapters/mysql2_adapter.rb +126 -0
- data/lib/schema_plus/foreign_keys/active_record/connection_adapters/postgresql_adapter.rb +89 -0
- data/lib/schema_plus/foreign_keys/active_record/connection_adapters/sqlite3_adapter.rb +77 -0
- data/lib/schema_plus/foreign_keys/active_record/connection_adapters/table_definition.rb +108 -0
- data/lib/schema_plus/foreign_keys/active_record/migration/command_recorder.rb +29 -0
- data/lib/schema_plus/foreign_keys/middleware/dumper.rb +88 -0
- data/lib/schema_plus/foreign_keys/middleware/migration.rb +147 -0
- data/lib/schema_plus/foreign_keys/middleware/model.rb +15 -0
- data/lib/schema_plus/foreign_keys/middleware/mysql.rb +20 -0
- data/lib/schema_plus/foreign_keys/middleware/sql.rb +27 -0
- data/lib/schema_plus/foreign_keys/version.rb +5 -0
- data/lib/schema_plus_foreign_keys.rb +1 -0
- data/schema_dev.yml +9 -0
- data/schema_plus_foreign_keys.gemspec +31 -0
- data/spec/deprecation_spec.rb +161 -0
- data/spec/foreign_key_definition_spec.rb +34 -0
- data/spec/foreign_key_spec.rb +207 -0
- data/spec/migration_spec.rb +570 -0
- data/spec/named_schemas_spec.rb +136 -0
- data/spec/schema_dumper_spec.rb +257 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/support/reference.rb +79 -0
- metadata +221 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
module SchemaPlus::ForeignKeys
|
2
|
+
module ActiveRecord
|
3
|
+
module ConnectionAdapters
|
4
|
+
|
5
|
+
# The Postgresql adapter implements the SchemaPlus::ForeignKeys extensions and
|
6
|
+
# enhancements
|
7
|
+
module PostgresqlAdapter
|
8
|
+
|
9
|
+
def rename_table(oldname, newname) #:nodoc:
|
10
|
+
super
|
11
|
+
rename_foreign_keys(oldname, newname)
|
12
|
+
end
|
13
|
+
|
14
|
+
def foreign_keys(table_name, name = nil) #:nodoc:
|
15
|
+
load_foreign_keys(<<-SQL, name)
|
16
|
+
SELECT f.conname, pg_get_constraintdef(f.oid), t.relname
|
17
|
+
FROM pg_class t, pg_constraint f
|
18
|
+
WHERE f.conrelid = t.oid
|
19
|
+
AND f.contype = 'f'
|
20
|
+
AND t.relname = '#{table_name_without_namespace(table_name)}'
|
21
|
+
AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{namespace_sql(table_name)} )
|
22
|
+
SQL
|
23
|
+
end
|
24
|
+
|
25
|
+
def reverse_foreign_keys(table_name, name = nil) #:nodoc:
|
26
|
+
load_foreign_keys(<<-SQL, name)
|
27
|
+
SELECT f.conname, pg_get_constraintdef(f.oid), t2.relname
|
28
|
+
FROM pg_class t, pg_class t2, pg_constraint f
|
29
|
+
WHERE f.confrelid = t.oid
|
30
|
+
AND f.conrelid = t2.oid
|
31
|
+
AND f.contype = 'f'
|
32
|
+
AND t.relname = '#{table_name_without_namespace(table_name)}'
|
33
|
+
AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{namespace_sql(table_name)} )
|
34
|
+
SQL
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def unquote(name)
|
40
|
+
return name.map { |name| unquote(name) } if name.is_a?(Array)
|
41
|
+
name.sub(/^["`](.*)["`]$/, '\1')
|
42
|
+
end
|
43
|
+
|
44
|
+
def namespace_sql(table_name)
|
45
|
+
(table_name.to_s =~ /(.*)[.]/) ? "'#{$1}'" : "ANY (current_schemas(false))"
|
46
|
+
end
|
47
|
+
|
48
|
+
def table_name_without_namespace(table_name)
|
49
|
+
table_name.to_s.sub /.*[.]/, ''
|
50
|
+
end
|
51
|
+
|
52
|
+
def load_foreign_keys(sql, name = nil) #:nodoc:
|
53
|
+
foreign_keys = []
|
54
|
+
|
55
|
+
query(sql, name).each do |row|
|
56
|
+
if row[1] =~ /^FOREIGN KEY \((.+?)\) REFERENCES (.+?)\((.+?)\)( ON UPDATE (.+?))?( ON DELETE (.+?))?( (DEFERRABLE|NOT DEFERRABLE)( (INITIALLY DEFERRED|INITIALLY IMMEDIATE))?)?$/
|
57
|
+
name = row[0]
|
58
|
+
from_table = unquote(row[2])
|
59
|
+
columns = unquote($1.split(', '))
|
60
|
+
to_table = unquote($2)
|
61
|
+
primary_keys = unquote($3.split(', '))
|
62
|
+
on_update = $5
|
63
|
+
on_delete = $7
|
64
|
+
deferrable = $9 == "DEFERRABLE"
|
65
|
+
deferrable = :initially_deferred if ($11 == "INITIALLY DEFERRED" )
|
66
|
+
on_update = ForeignKeyDefinition::ACTION_LOOKUP[on_update] || :no_action
|
67
|
+
on_delete = ForeignKeyDefinition::ACTION_LOOKUP[on_delete] || :no_action
|
68
|
+
|
69
|
+
options = { :name => name,
|
70
|
+
:on_delete => on_delete,
|
71
|
+
:on_update => on_update,
|
72
|
+
:column => columns,
|
73
|
+
:primary_key => primary_keys,
|
74
|
+
:deferrable => deferrable }
|
75
|
+
|
76
|
+
foreign_keys << ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
|
77
|
+
from_table,
|
78
|
+
to_table.sub(/^"(.*)"$/, '\1'),
|
79
|
+
options)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
foreign_keys
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module SchemaPlus::ForeignKeys
|
2
|
+
module ActiveRecord
|
3
|
+
module ConnectionAdapters
|
4
|
+
|
5
|
+
# SchemaPlus::ForeignKeys includes an Sqlite3 implementation of the AbstractAdapter
|
6
|
+
# extensions.
|
7
|
+
module Sqlite3Adapter
|
8
|
+
|
9
|
+
# :enddoc:
|
10
|
+
|
11
|
+
def add_foreign_key(table_name, to_table, options = {})
|
12
|
+
raise NotImplementedError, "Sqlite3 does not support altering a table to add foreign key constraints (table #{table_name.inspect} to #{to_table.inspect})"
|
13
|
+
end
|
14
|
+
|
15
|
+
def remove_foreign_key(table_name, *args)
|
16
|
+
raise NotImplementedError, "Sqlite3 does not support altering a table to remove foreign key constraints (table #{table_name.inspect} constraint #{args.inspect})"
|
17
|
+
end
|
18
|
+
|
19
|
+
def foreign_keys(table_name, name = nil)
|
20
|
+
get_foreign_keys(table_name, name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def reverse_foreign_keys(table_name, name = nil)
|
24
|
+
get_foreign_keys(nil, name).select{|definition| definition.to_table == table_name}
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def get_foreign_keys(table_name = nil, name = nil)
|
30
|
+
results = execute(<<-SQL, name)
|
31
|
+
SELECT name, sql FROM sqlite_master
|
32
|
+
WHERE type='table' #{table_name && %" AND name='#{table_name}' "}
|
33
|
+
SQL
|
34
|
+
|
35
|
+
re = %r[
|
36
|
+
\b(CONSTRAINT\s+(\S+)\s+)?
|
37
|
+
FOREIGN\s+KEY\s* \(\s*[`"](.+?)[`"]\s*\)
|
38
|
+
\s*REFERENCES\s*[`"](.+?)[`"]\s*\((.+?)\)
|
39
|
+
(\s+ON\s+UPDATE\s+(.+?))?
|
40
|
+
(\s*ON\s+DELETE\s+(.+?))?
|
41
|
+
(\s*DEFERRABLE(\s+INITIALLY\s+DEFERRED)?)?
|
42
|
+
\s*[,)]
|
43
|
+
]x
|
44
|
+
|
45
|
+
foreign_keys = []
|
46
|
+
results.each do |row|
|
47
|
+
from_table = row["name"]
|
48
|
+
row["sql"].scan(re).each do |d0, name, columns, to_table, primary_keys, d1, on_update, d2, on_delete, deferrable, initially_deferred|
|
49
|
+
columns = columns.gsub(/`/, '').split(', ')
|
50
|
+
|
51
|
+
primary_keys = primary_keys.gsub(/[`"]/, '').split(', ')
|
52
|
+
on_update = ForeignKeyDefinition::ACTION_LOOKUP[on_update] || :no_action
|
53
|
+
on_delete = ForeignKeyDefinition::ACTION_LOOKUP[on_delete] || :no_action
|
54
|
+
deferrable = deferrable ? (initially_deferred ? :initially_deferred : true) : false
|
55
|
+
|
56
|
+
options = { :name => name,
|
57
|
+
:on_update => on_update,
|
58
|
+
:on_delete => on_delete,
|
59
|
+
:column => columns,
|
60
|
+
:primary_key => primary_keys,
|
61
|
+
:deferrable => deferrable }
|
62
|
+
|
63
|
+
foreign_keys << ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
|
64
|
+
from_table,
|
65
|
+
to_table,
|
66
|
+
options)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
foreign_keys
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module SchemaPlus::ForeignKeys::ActiveRecord::ConnectionAdapters
|
2
|
+
|
3
|
+
#
|
4
|
+
# SchemaPlus::ForeignKeys adds several methods to TableDefinition, allowing indexes
|
5
|
+
# and foreign key constraints to be defined within a
|
6
|
+
# <tt>create_table</tt> block of a migration, allowing for better
|
7
|
+
# encapsulation and more DRY definitions.
|
8
|
+
#
|
9
|
+
# For example, without SchemaPlus::ForeignKeys you might define a table like this:
|
10
|
+
#
|
11
|
+
# create_table :widgets do |t|
|
12
|
+
# t.string :name
|
13
|
+
# end
|
14
|
+
# add_index :widgets, :name
|
15
|
+
#
|
16
|
+
# But with SchemaPlus::ForeignKeys, the index can be defined within the create_table
|
17
|
+
# block, so you don't need to repeat the table name:
|
18
|
+
#
|
19
|
+
# create_table :widgets do |t|
|
20
|
+
# t.string :name
|
21
|
+
# t.index :name
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Even more DRY, you can define the index as part of the column
|
25
|
+
# definition, via:
|
26
|
+
#
|
27
|
+
# create_table :widgets do |t|
|
28
|
+
# t.string :name, :index => true
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# For details about the :index option (including unique and multi-column indexes), see the
|
32
|
+
# documentation for Migration::ClassMethods#add_column
|
33
|
+
#
|
34
|
+
# SchemaPlus::ForeignKeys also supports creation of foreign key constraints analogously, using Migration::ClassMethods#add_foreign_key or TableDefinition#foreign_key or as part of the column definition, for example:
|
35
|
+
#
|
36
|
+
# create_table :posts do |t| # not DRY
|
37
|
+
# t.integer :author_id
|
38
|
+
# end
|
39
|
+
# add_foreign_key :posts, :author_id, :references => :authors
|
40
|
+
#
|
41
|
+
# create_table :posts do |t| # DRYer
|
42
|
+
# t.integer :author_id
|
43
|
+
# t.foreign_key :author_id, :references => :authors
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# create_table :posts do |t| # Dryest
|
47
|
+
# t.integer :author_id, :foreign_key => true
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# <b>NOTE:</b> In the standard configuration, SchemaPlus::ForeignKeys automatically
|
51
|
+
# creates foreign key constraints for columns whose names end in
|
52
|
+
# <tt>_id</tt>. So the above examples are redundant, unless automatic
|
53
|
+
# creation was disabled at initialization in the global Config.
|
54
|
+
#
|
55
|
+
# SchemaPlus::ForeignKeys likewise by default automatically creates foreign key constraints for
|
56
|
+
# columns defined via <tt>t.references</tt>. However, SchemaPlus::ForeignKeys does not create
|
57
|
+
# foreign key constraints if the <tt>:polymorphic</tt> option is true
|
58
|
+
#
|
59
|
+
# Finally, the configuration for foreign keys can be overriden on a per-table
|
60
|
+
# basis by passing Config options to Migration::ClassMethods#create_table, such as
|
61
|
+
#
|
62
|
+
# create_table :students, :foreign_keys => {:auto_create => false} do
|
63
|
+
# t.integer :student_id
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
module TableDefinition
|
67
|
+
|
68
|
+
attr_accessor :schema_plus_foreign_keys_config #:nodoc:
|
69
|
+
|
70
|
+
if ActiveRecord.version == Gem::Version.new("4.2.0")
|
71
|
+
def foreign_keys
|
72
|
+
@foreign_keys ||= []
|
73
|
+
end
|
74
|
+
|
75
|
+
def foreign_keys_for_table(*)
|
76
|
+
foreign_keys
|
77
|
+
end
|
78
|
+
else
|
79
|
+
def foreign_keys_for_table(table)
|
80
|
+
foreign_keys[table] ||= []
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def foreign_key(*args) # (column_names, to_table, primary_key=nil, options=nil)
|
85
|
+
options = args.extract_options!
|
86
|
+
case args.length
|
87
|
+
when 1
|
88
|
+
to_table = args[0]
|
89
|
+
column_names = "#{to_table.to_s.singularize}_id"
|
90
|
+
when 2
|
91
|
+
column_names, to_table = args
|
92
|
+
when 3
|
93
|
+
ActiveSupport::Deprecation.warn "positional arg for foreign primary key is deprecated, use :primary_key option instead"
|
94
|
+
column_names, to_table, primary_key = args
|
95
|
+
options.merge!(:primary_key => primary_key)
|
96
|
+
else
|
97
|
+
raise ArgumentError, "wrong number of arguments (#{args.length}) for foreign_key(column_names, table_name, options)"
|
98
|
+
end
|
99
|
+
|
100
|
+
options.merge!(:column => column_names)
|
101
|
+
options.reverse_merge!(:name => ForeignKeyDefinition.default_name(self.name, column_names))
|
102
|
+
fk = ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(self.name, AbstractAdapter.proper_table_name(to_table), options)
|
103
|
+
foreign_keys_for_table(fk.to_table) << fk
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module SchemaPlus::ForeignKeys
|
2
|
+
module ActiveRecord
|
3
|
+
module Migration
|
4
|
+
module CommandRecorder
|
5
|
+
|
6
|
+
attr_accessor :schema_plus_foreign_keys_config #:nodoc:
|
7
|
+
|
8
|
+
# seems like this is fixing a rails bug:
|
9
|
+
# change_table foo, :bulk => true { |t| t.references :bar }
|
10
|
+
# results in an 'unknown method :add_reference_sql' (with mysql2)
|
11
|
+
#
|
12
|
+
# should track it down separately and submit a patch/fix to rails
|
13
|
+
#
|
14
|
+
def add_reference(table_name, ref_name, options = {}) #:nodoc:
|
15
|
+
polymorphic = options.delete(:polymorphic)
|
16
|
+
options[:references] = nil if polymorphic
|
17
|
+
# ugh. copying and pasting code from ::ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference
|
18
|
+
index_options = options.delete(:index)
|
19
|
+
add_column(table_name, "#{ref_name}_id", :integer, options)
|
20
|
+
add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
|
21
|
+
add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
|
22
|
+
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module SchemaPlus::ForeignKeys
|
2
|
+
module Middleware
|
3
|
+
module Dumper
|
4
|
+
|
5
|
+
# index and foreign key constraint definitions are dumped
|
6
|
+
# inline in the create_table block. (This is done for elegance, but
|
7
|
+
# also because Sqlite3 does not allow foreign key constraints to be
|
8
|
+
# added to a table after it has been defined.)
|
9
|
+
|
10
|
+
module Tables
|
11
|
+
|
12
|
+
def before(env)
|
13
|
+
|
14
|
+
@inline_fks = Hash.new{ |h, k| h[k] = [] }
|
15
|
+
@backref_fks = Hash.new{ |h, k| h[k] = [] }
|
16
|
+
|
17
|
+
env.connection.tables.each do |table|
|
18
|
+
if (fks = env.connection.foreign_keys(table)).any?
|
19
|
+
env.dump.data.has_fks = true
|
20
|
+
@inline_fks[table] = fks
|
21
|
+
env.dump.depends(table, fks.collect(&:to_table))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Normally we dump foreign key constraints inline in the table
|
26
|
+
# definitions, both for visual cleanliness and because sqlite3
|
27
|
+
# doesn't allow foreign key constraints to be added afterwards.
|
28
|
+
# But in case there's a cycle in the constraint references, some
|
29
|
+
# constraints will need to be broken out then added later. (Adding
|
30
|
+
# constraints later won't work with sqlite3, but that means sqlite3
|
31
|
+
# won't let you create cycles in the first place.)
|
32
|
+
break_fk_cycles(env) while env.dump.strongly_connected_components.any?{|component| component.size > 1}
|
33
|
+
|
34
|
+
env.dump.data.inline_fks = @inline_fks
|
35
|
+
env.dump.data.backref_fks = @backref_fks
|
36
|
+
end
|
37
|
+
|
38
|
+
# Ignore the foreign key dumps at the end of the schema; we'll put them in/near their tables
|
39
|
+
def after(env)
|
40
|
+
env.dump.final.reject!(&it =~/foreign_key/)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def break_fk_cycles(env) #:nodoc:
|
46
|
+
env.dump.strongly_connected_components.select{|component| component.size > 1}.each do |tables|
|
47
|
+
table = tables.sort.last
|
48
|
+
backref_fks = @inline_fks[table].select{|fk| tables.include?(fk.to_table)}
|
49
|
+
@inline_fks[table] -= backref_fks
|
50
|
+
env.dump.dependencies[table] -= backref_fks.collect(&:to_table)
|
51
|
+
backref_fks.each do |fk|
|
52
|
+
@backref_fks[fk.to_table] << fk
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module SQLite3
|
58
|
+
def after(env)
|
59
|
+
env.dump.initial << " PRAGMA FOREIGN_KEYS = ON;\n" if env.dump.data.has_fks
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
module Table
|
66
|
+
def after(env)
|
67
|
+
dumped = {}
|
68
|
+
env.table.columns.each do |column|
|
69
|
+
if (foreign_key = env.dump.data.inline_fks[env.table.name].find(&its.column.to_s == column.name))
|
70
|
+
column.add_option foreign_key.to_dump(column: true)
|
71
|
+
dumped[foreign_key] = true
|
72
|
+
end
|
73
|
+
if (foreign_key = env.dump.data.backref_fks.values.flatten.find{|fk| fk.from_table.to_s == env.table.name && fk.column.to_s == column.name})
|
74
|
+
column.add_comment "foreign key references #{foreign_key.to_table.inspect} (below)"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
env.table.trailer += env.dump.data.inline_fks[env.table.name].map { |foreign_key|
|
78
|
+
foreign_key.to_dump unless dumped[foreign_key] # just in case we missed any. don't think it can happen
|
79
|
+
}.compact.sort
|
80
|
+
env.table.trailer += env.dump.data.backref_fks[env.table.name].map { |foreign_key|
|
81
|
+
foreign_key.to_dump
|
82
|
+
}.sort
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module SchemaPlus::ForeignKeys
|
2
|
+
module Middleware
|
3
|
+
module Migration
|
4
|
+
|
5
|
+
module CreateTable
|
6
|
+
def around(env)
|
7
|
+
if (original_block = env.block)
|
8
|
+
config_options = env.options.delete(:foreign_keys) || {}
|
9
|
+
env.block = -> (table_definition) {
|
10
|
+
table_definition.schema_plus_foreign_keys_config = SchemaPlus::ForeignKeys.config.merge(config_options)
|
11
|
+
original_block.call table_definition
|
12
|
+
}
|
13
|
+
end
|
14
|
+
yield env
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Column
|
19
|
+
|
20
|
+
#
|
21
|
+
# Column option shortcuts
|
22
|
+
#
|
23
|
+
def before(env)
|
24
|
+
opts = env.options[:foreign_key]
|
25
|
+
|
26
|
+
return if opts == false
|
27
|
+
|
28
|
+
opts = {} if opts == true
|
29
|
+
|
30
|
+
[:references, :on_update, :on_delete, :deferrable].each do |key|
|
31
|
+
(opts||={}).reverse_merge!(key => env.options[key]) if env.options.has_key? key
|
32
|
+
end
|
33
|
+
|
34
|
+
return if opts.nil?
|
35
|
+
|
36
|
+
if opts.has_key?(:references) && !opts[:references]
|
37
|
+
env.options[:foreign_key] = false
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
case opts[:references]
|
42
|
+
when nil
|
43
|
+
when Array
|
44
|
+
table, primary_key = opts[:references]
|
45
|
+
opts[:references] = table
|
46
|
+
opts[:primary_key] ||= primary_key
|
47
|
+
end
|
48
|
+
|
49
|
+
env.options[:foreign_key] = opts
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Add the foreign keys
|
54
|
+
#
|
55
|
+
def around(env)
|
56
|
+
original_options = env.options
|
57
|
+
env.options = original_options.dup
|
58
|
+
|
59
|
+
is_reference = (env.type == :reference)
|
60
|
+
is_polymorphic = is_reference && env.options[:polymorphic]
|
61
|
+
|
62
|
+
# usurp foreign key creation from AR, since it doesn't support
|
63
|
+
# all our features
|
64
|
+
env.options[:foreign_key] = false
|
65
|
+
|
66
|
+
yield env
|
67
|
+
|
68
|
+
return if is_polymorphic or env.implements_reference
|
69
|
+
|
70
|
+
env.options = original_options
|
71
|
+
|
72
|
+
add_foreign_keys(env)
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def add_foreign_keys(env)
|
79
|
+
|
80
|
+
if (reverting = env.caller.is_a?(::ActiveRecord::Migration::CommandRecorder) && env.caller.reverting)
|
81
|
+
commands_length = env.caller.commands.length
|
82
|
+
end
|
83
|
+
|
84
|
+
config = (env.caller.try(:schema_plus_foreign_keys_config) || SchemaPlus::ForeignKeys.config)
|
85
|
+
fk_opts = get_fk_opts(env, config)
|
86
|
+
|
87
|
+
# remove existing fk in case of change of fk on existing column
|
88
|
+
if env.operation == :change and fk_opts # includes :none for explicitly off
|
89
|
+
remove_foreign_key_if_exists(env)
|
90
|
+
end
|
91
|
+
|
92
|
+
fk_opts = nil if fk_opts == :none
|
93
|
+
|
94
|
+
create_fk(env, fk_opts) if fk_opts
|
95
|
+
|
96
|
+
if reverting
|
97
|
+
rev = []
|
98
|
+
while env.caller.commands.length > commands_length
|
99
|
+
cmd = env.caller.commands.pop
|
100
|
+
rev.unshift cmd unless cmd[0].to_s =~ /^add_/
|
101
|
+
end
|
102
|
+
env.caller.commands.concat rev
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
def create_fk(env, fk_opts)
|
108
|
+
references = fk_opts.delete(:references)
|
109
|
+
case env.caller
|
110
|
+
when ::ActiveRecord::ConnectionAdapters::TableDefinition
|
111
|
+
env.caller.foreign_key(env.column_name, references, fk_opts)
|
112
|
+
else
|
113
|
+
env.caller.add_foreign_key(env.table_name, references, fk_opts.merge(:column => env.column_name))
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def get_fk_opts(env, config)
|
118
|
+
opts = env.options[:foreign_key]
|
119
|
+
return nil if opts.nil?
|
120
|
+
return :none if opts == false
|
121
|
+
opts = {} if opts == true
|
122
|
+
opts[:references] ||= default_table_name(env)
|
123
|
+
opts[:on_update] ||= config.on_update
|
124
|
+
opts[:on_delete] ||= config.on_delete
|
125
|
+
opts
|
126
|
+
end
|
127
|
+
|
128
|
+
def remove_foreign_key_if_exists(env)
|
129
|
+
env.caller.remove_foreign_key(env.table_name.to_s, column: env.column_name.to_s, :if_exists => true)
|
130
|
+
end
|
131
|
+
|
132
|
+
def default_table_name(env)
|
133
|
+
if env.column_name.to_s == 'parent_id'
|
134
|
+
env.table_name
|
135
|
+
else
|
136
|
+
name = env.column_name.to_s.sub(/_id$/, '')
|
137
|
+
name = name.pluralize if ::ActiveRecord::Base.pluralize_table_names
|
138
|
+
name
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|