schema_plus 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.gitignore +25 -0
  2. data/Gemfile +3 -0
  3. data/MIT-LICENSE +25 -0
  4. data/README.rdoc +147 -0
  5. data/Rakefile +70 -0
  6. data/init.rb +1 -0
  7. data/lib/schema_plus/active_record/associations.rb +211 -0
  8. data/lib/schema_plus/active_record/base.rb +81 -0
  9. data/lib/schema_plus/active_record/connection_adapters/abstract_adapter.rb +96 -0
  10. data/lib/schema_plus/active_record/connection_adapters/column.rb +55 -0
  11. data/lib/schema_plus/active_record/connection_adapters/foreign_key_definition.rb +115 -0
  12. data/lib/schema_plus/active_record/connection_adapters/index_definition.rb +51 -0
  13. data/lib/schema_plus/active_record/connection_adapters/mysql_adapter.rb +111 -0
  14. data/lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb +163 -0
  15. data/lib/schema_plus/active_record/connection_adapters/schema_statements.rb +39 -0
  16. data/lib/schema_plus/active_record/connection_adapters/sqlite3_adapter.rb +78 -0
  17. data/lib/schema_plus/active_record/connection_adapters/table_definition.rb +130 -0
  18. data/lib/schema_plus/active_record/migration.rb +220 -0
  19. data/lib/schema_plus/active_record/schema.rb +27 -0
  20. data/lib/schema_plus/active_record/schema_dumper.rb +122 -0
  21. data/lib/schema_plus/active_record/validations.rb +139 -0
  22. data/lib/schema_plus/railtie.rb +12 -0
  23. data/lib/schema_plus/version.rb +3 -0
  24. data/lib/schema_plus.rb +248 -0
  25. data/schema_plus.gemspec +37 -0
  26. data/schema_plus.gemspec.rails3.0 +36 -0
  27. data/schema_plus.gemspec.rails3.1 +36 -0
  28. data/spec/association_spec.rb +529 -0
  29. data/spec/connections/mysql/connection.rb +18 -0
  30. data/spec/connections/mysql2/connection.rb +18 -0
  31. data/spec/connections/postgresql/connection.rb +15 -0
  32. data/spec/connections/sqlite3/connection.rb +14 -0
  33. data/spec/foreign_key_definition_spec.rb +23 -0
  34. data/spec/foreign_key_spec.rb +142 -0
  35. data/spec/index_definition_spec.rb +139 -0
  36. data/spec/index_spec.rb +71 -0
  37. data/spec/migration_spec.rb +405 -0
  38. data/spec/models/comment.rb +2 -0
  39. data/spec/models/post.rb +2 -0
  40. data/spec/models/user.rb +2 -0
  41. data/spec/references_spec.rb +78 -0
  42. data/spec/schema/auto_schema.rb +23 -0
  43. data/spec/schema/core_schema.rb +21 -0
  44. data/spec/schema_dumper_spec.rb +167 -0
  45. data/spec/schema_spec.rb +71 -0
  46. data/spec/spec_helper.rb +59 -0
  47. data/spec/support/extensions/active_model.rb +13 -0
  48. data/spec/support/helpers.rb +16 -0
  49. data/spec/support/matchers/automatic_foreign_key_matchers.rb +2 -0
  50. data/spec/support/matchers/have_index.rb +52 -0
  51. data/spec/support/matchers/reference.rb +66 -0
  52. data/spec/support/reference.rb +66 -0
  53. data/spec/validations_spec.rb +294 -0
  54. data/spec/views_spec.rb +140 -0
  55. metadata +269 -0
@@ -0,0 +1,163 @@
1
+ module SchemaPlus
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+ # The Postgresql adapter implements the SchemaPlus extensions and
5
+ # enhancements
6
+ module PostgresqlAdapter
7
+
8
+ def self.included(base) #:nodoc:
9
+ base.class_eval do
10
+ remove_method :indexes
11
+ end
12
+ end
13
+
14
+ # SchemaPlus provides the following extra options for Postgres
15
+ # indexes:
16
+ # * +:conditions+ - SQL conditions for the WHERE clause of the index
17
+ # * +:expression+ - SQL expression to index. column_name can be nil or ommitted, in which case :name must be provided
18
+ # * +:kind+ - index method for Postgresql to use
19
+ # * +:case_sensitive - if +false+ then the index will be created on LOWER(column_name)
20
+ #
21
+ # The <tt>:case_sensitive => false</tt> option ties in with Rails built-in support for case-insensitive searching:
22
+ # validates_uniqueness_of :name, :case_sensitive => false
23
+ #
24
+ def add_index(table_name, column_name, options = {})
25
+ column_name, options = [], column_name if column_name.is_a?(Hash)
26
+ column_names = Array(column_name).compact
27
+ if column_names.empty?
28
+ raise ArgumentError, "No columns and :expression missing from options - cannot create index" if options[:expression].blank?
29
+ raise ArgumentError, "Index name not given. Pass :name option" if options[:name].blank?
30
+ end
31
+
32
+ index_type = options[:unique] ? "UNIQUE" : ""
33
+ index_name = options[:name] || index_name(table_name, column_names)
34
+ conditions = options[:conditions]
35
+
36
+ if options[:expression] then
37
+ sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{options[:expression]}"
38
+ else
39
+ quoted_column_names = column_names.map { |e| options[:case_sensitive] == false && e.to_s !~ /_id$/ ? "LOWER(#{quote_column_name(e)})" : quote_column_name(e) }
40
+
41
+ sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names.join(", ")})"
42
+ sql += " WHERE (#{ ::ActiveRecord::Base.send(:sanitize_sql, conditions, quote_table_name(table_name)) })" if conditions
43
+ end
44
+ execute sql
45
+ end
46
+
47
+ def supports_partial_indexes? #:nodoc:
48
+ true
49
+ end
50
+
51
+ def indexes(table_name, name = nil) #:nodoc:
52
+ schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
53
+ result = query(<<-SQL, name)
54
+ SELECT distinct i.relname, d.indisunique, d.indkey, m.amname, t.oid,
55
+ pg_get_expr(d.indpred, t.oid), pg_get_expr(d.indexprs, t.oid)
56
+ FROM pg_class t, pg_class i, pg_index d, pg_am m
57
+ WHERE i.relkind = 'i'
58
+ AND i.relam = m.oid
59
+ AND d.indexrelid = i.oid
60
+ AND d.indisprimary = 'f'
61
+ AND t.oid = d.indrelid
62
+ AND t.relname = '#{table_name}'
63
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) )
64
+ ORDER BY i.relname
65
+ SQL
66
+
67
+ result.map do |(index_name, is_unique, indkey, kind, oid, conditions, expression)|
68
+ unique = (is_unique == 't')
69
+ index_keys = indkey.split(" ")
70
+
71
+ columns = Hash[query(<<-SQL, "Columns for index #{index_name} on #{table_name}")]
72
+ SELECT a.attnum, a.attname
73
+ FROM pg_attribute a
74
+ WHERE a.attrelid = #{oid}
75
+ AND a.attnum IN (#{index_keys.join(",")})
76
+ SQL
77
+
78
+ column_names = columns.values_at(*index_keys).compact
79
+ if md = expression.try(:match, /^lower\(\(?([^)]+)\)?(::text)?\)$/i)
80
+ column_names << md[1]
81
+ end
82
+ ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, column_names,
83
+ :name => index_name,
84
+ :unique => unique,
85
+ :conditions => conditions,
86
+ :case_sensitive => !(expression =~ /lower/i),
87
+ :kind => kind.downcase == "btree" ? nil : kind,
88
+ :expression => expression)
89
+ end
90
+ end
91
+
92
+ def foreign_keys(table_name, name = nil) #:nodoc:
93
+ load_foreign_keys(<<-SQL, name)
94
+ SELECT f.conname, pg_get_constraintdef(f.oid), t.relname
95
+ FROM pg_class t, pg_constraint f
96
+ WHERE f.conrelid = t.oid
97
+ AND f.contype = 'f'
98
+ AND t.relname = '#{table_name}'
99
+ SQL
100
+ end
101
+
102
+ def reverse_foreign_keys(table_name, name = nil) #:nodoc:
103
+ load_foreign_keys(<<-SQL, name)
104
+ SELECT f.conname, pg_get_constraintdef(f.oid), t2.relname
105
+ FROM pg_class t, pg_class t2, pg_constraint f
106
+ WHERE f.confrelid = t.oid
107
+ AND f.conrelid = t2.oid
108
+ AND f.contype = 'f'
109
+ AND t.relname = '#{table_name}'
110
+ SQL
111
+ end
112
+
113
+ def views(name = nil) #:nodoc:
114
+ schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
115
+ query(<<-SQL, name).map { |row| row[0] }
116
+ SELECT viewname
117
+ FROM pg_views
118
+ WHERE schemaname IN (#{schemas})
119
+ SQL
120
+ end
121
+
122
+ def view_definition(view_name, name = nil) #:nodoc:
123
+ result = query(<<-SQL, name)
124
+ SELECT pg_get_viewdef(oid)
125
+ FROM pg_class
126
+ WHERE relkind = 'v'
127
+ AND relname = '#{view_name}'
128
+ SQL
129
+ row = result.first
130
+ row.first.chomp(';') unless row.nil?
131
+ end
132
+
133
+ private
134
+
135
+ def load_foreign_keys(sql, name = nil) #:nodoc:
136
+ foreign_keys = []
137
+
138
+ query(sql, name).each do |row|
139
+ if row[1] =~ /^FOREIGN KEY \((.+?)\) REFERENCES (.+?)\((.+?)\)( ON UPDATE (.+?))?( ON DELETE (.+?))?( (DEFERRABLE|NOT DEFERRABLE))?$/
140
+ name = row[0]
141
+ from_table_name = row[2]
142
+ column_names = $1
143
+ references_table_name = $2
144
+ references_column_names = $3
145
+ on_update = $5
146
+ on_delete = $7
147
+ deferrable = $9 == "DEFERRABLE"
148
+ on_update = on_update ? on_update.downcase.gsub(' ', '_').to_sym : :no_action
149
+ on_delete = on_delete ? on_delete.downcase.gsub(' ', '_').to_sym : :no_action
150
+
151
+ foreign_keys << ForeignKeyDefinition.new(name,
152
+ from_table_name, column_names.split(', '),
153
+ references_table_name.sub(/^"(.*)"$/, '\1'), references_column_names.split(', '),
154
+ on_update, on_delete, deferrable)
155
+ end
156
+ end
157
+
158
+ foreign_keys
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,39 @@
1
+ module SchemaPlus::ActiveRecord::ConnectionAdapters
2
+ module SchemaStatements
3
+
4
+ def self.included(base) #:nodoc:
5
+ base.class_eval do
6
+ alias_method_chain :create_table, :schema_plus
7
+ end
8
+ end
9
+
10
+ ##
11
+ # :method: create_table
12
+ #
13
+ # SchemaPlus extends SchemaStatements::create_table to allow you to specify configuration options per table. Pass them in as a hash keyed by configuration set (see SchemaPlus::Config),
14
+ # for example:
15
+ #
16
+ # create_table :widgets, :foreign_keys => {:auto_create => true, :on_delete => :cascade} do |t|
17
+ # ...
18
+ # end
19
+ def create_table_with_schema_plus(table, options = {})
20
+ options = options.dup
21
+ config_options = {}
22
+ options.keys.each { |key| config_options[key] = options.delete(key) if SchemaPlus.config.class.attributes.include? key }
23
+
24
+ indexes = []
25
+ create_table_without_schema_plus(table, options) do |table_definition|
26
+ table_definition.schema_plus_config = SchemaPlus.config.merge(config_options)
27
+ table_definition.name = table
28
+ yield table_definition if block_given?
29
+ indexes = table_definition.indexes
30
+ end
31
+ indexes.each do |index|
32
+ add_index(table, index.columns, index.opts)
33
+ end
34
+
35
+
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,78 @@
1
+ module SchemaPlus
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+ # SchemaPlus includes an Sqlite3 implementation of the AbstractAdapater
5
+ # extensions.
6
+ module Sqlite3Adapter
7
+
8
+ # :enddoc:
9
+
10
+ def add_foreign_key(table_name, column_names, references_table_name, references_column_names, options = {})
11
+ raise NotImplementedError, "Sqlite3 does not support altering a table to add foreign key constraints (table #{table_name.inspect} column #{column_names.inspect})"
12
+ end
13
+
14
+ def remove_foreign_key(table_name, foreign_key_name)
15
+ raise NotImplementedError, "Sqlite3 does not support altering a table to remove foreign key constraints (table #{table_name.inspect} constraint #{foreign_key_name.inspect})"
16
+ end
17
+
18
+ def foreign_keys(table_name, name = nil)
19
+ get_foreign_keys(table_name, name)
20
+ end
21
+
22
+ def reverse_foreign_keys(table_name, name = nil)
23
+ get_foreign_keys(nil, name).select{|definition| definition.references_table_name == table_name}
24
+ end
25
+
26
+ def views(name = nil)
27
+ execute("SELECT name FROM sqlite_master WHERE type='view'", name).collect{|row| row["name"]}
28
+ end
29
+
30
+ def view_definition(view_name, name = nil)
31
+ sql = execute("SELECT sql FROM sqlite_master WHERE type='view' AND name=#{quote(view_name)}", name).collect{|row| row["sql"]}.first
32
+ sql.sub(/^CREATE VIEW \S* AS\s+/im, '') unless sql.nil?
33
+ end
34
+
35
+ protected
36
+
37
+ def post_initialize
38
+ execute('PRAGMA FOREIGN_KEYS = 1')
39
+ end
40
+
41
+ def get_foreign_keys(table_name = nil, name = nil)
42
+ results = execute(<<-SQL, name)
43
+ SELECT name, sql FROM sqlite_master
44
+ WHERE type='table' #{table_name && %" AND name='#{table_name}' "}
45
+ SQL
46
+
47
+ re = %r[
48
+ \bFOREIGN\s+KEY\s* \(\s*[`"](.+?)[`"]\s*\)
49
+ \s*REFERENCES\s*[`"](.+?)[`"]\s*\((.+?)\)
50
+ (\s+ON\s+UPDATE\s+(.+?))?
51
+ (\s*ON\s+DELETE\s+(.+?))?
52
+ \s*[,)]
53
+ ]x
54
+
55
+ foreign_keys = []
56
+ results.each do |row|
57
+ table_name = row["name"]
58
+ row["sql"].scan(re).each do |column_names, references_table_name, references_column_names, d1, on_update, d2, on_delete|
59
+ column_names = column_names.gsub('`', '').split(', ')
60
+
61
+ references_column_names = references_column_names.gsub('`"', '').split(', ')
62
+ on_update = on_update ? on_update.downcase.gsub(' ', '_').to_sym : :no_action
63
+ on_delete = on_delete ? on_delete.downcase.gsub(' ', '_').to_sym : :no_action
64
+ foreign_keys << ForeignKeyDefinition.new(nil,
65
+ table_name, column_names,
66
+ references_table_name, references_column_names,
67
+ on_update, on_delete)
68
+ end
69
+ end
70
+
71
+ foreign_keys
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,130 @@
1
+ module SchemaPlus::ActiveRecord::ConnectionAdapters
2
+
3
+ #
4
+ # SchemaPlus 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 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, 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 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, :references => :authors
48
+ # end
49
+ #
50
+ # <b>NOTE:</b> In the standard configuration, SchemaPlus 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
+ # Finally, the configuration for foreign keys can be overriden on a per-table
56
+ # basis by passing Config options to Migration::ClassMethods#create_table, such as
57
+ #
58
+ # create_table :students, :foreign_keys => {:auto_create => false} do
59
+ # t.integer :student_id
60
+ # end
61
+ #
62
+ module TableDefinition
63
+
64
+ attr_accessor :schema_plus_config #:nodoc:
65
+
66
+ def self.included(base) #:nodoc:
67
+ base.class_eval do
68
+ attr_accessor :name
69
+ attr_accessor :indexes
70
+ alias_method_chain :initialize, :schema_plus
71
+ alias_method_chain :column, :schema_plus
72
+ alias_method_chain :primary_key, :schema_plus
73
+ alias_method_chain :to_sql, :schema_plus
74
+ end
75
+ end
76
+
77
+ def initialize_with_schema_plus(*args) #:nodoc:
78
+ initialize_without_schema_plus(*args)
79
+ @foreign_keys = []
80
+ @indexes = []
81
+ end
82
+
83
+ def primary_key_with_schema_plus(name, options = {}) #:nodoc:
84
+ column(name, :primary_key, options)
85
+ end
86
+
87
+ def column_with_schema_plus(name, type, options = {}) #:nodoc:
88
+ column_without_schema_plus(name, type, options)
89
+ if references = ActiveRecord::Migration.get_references(self.name, name, options, schema_plus_config)
90
+ if index = options.fetch(:index, fk_use_auto_index?)
91
+ self.column_index(name, index)
92
+ end
93
+ foreign_key(name, references.first, references.last,
94
+ options.reverse_merge(:on_update => schema_plus_config.foreign_keys.on_update,
95
+ :on_delete => schema_plus_config.foreign_keys.on_delete))
96
+ elsif options[:index]
97
+ self.column_index(name, options[:index])
98
+ end
99
+ self
100
+ end
101
+
102
+ def to_sql_with_schema_plus #:nodoc:
103
+ sql = to_sql_without_schema_plus
104
+ sql << ', ' << @foreign_keys.map(&:to_sql) * ', ' unless @foreign_keys.empty?
105
+ sql
106
+ end
107
+
108
+ # Define an index for the current
109
+ def index(column_name, options={})
110
+ @indexes << ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(self.name, column_name, options)
111
+ end
112
+
113
+ def foreign_key(column_names, references_table_name, references_column_names, options = {})
114
+ @foreign_keys << ForeignKeyDefinition.new(options[:name], nil, column_names, ::ActiveRecord::Migrator.proper_table_name(references_table_name), references_column_names, options[:on_update], options[:on_delete], options[:deferrable])
115
+ self
116
+ end
117
+
118
+ protected
119
+ def column_index(name, options) #:nodoc:
120
+ options = {} if options == true
121
+ name = [name] + Array.wrap(options.delete(:with)).compact
122
+ self.index(name, options)
123
+ end
124
+
125
+ def fk_use_auto_index? #:nodoc:
126
+ schema_plus_config.foreign_keys.auto_index? && !ActiveRecord::Schema.defining?
127
+ end
128
+
129
+ end
130
+ end
@@ -0,0 +1,220 @@
1
+ module SchemaPlus::ActiveRecord
2
+ # SchemaPlus extends ActiveRecord::Migration with several enhancements. See documentation at Migration::ClassMethods
3
+ module Migration
4
+ def self.included(base) #:nodoc:
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ #
9
+ # SchemaPlus extends ActiveRecord::Migration with the following enhancements.
10
+ #
11
+ module ClassMethods
12
+
13
+ # Create a new view, given its name and SQL definition
14
+ #
15
+ def create_view(view_name, definition)
16
+ connection.create_view(view_name, definition)
17
+ end
18
+
19
+ # Drop the named view
20
+ def drop_view(view_name)
21
+ connection.drop_view(view_name)
22
+ end
23
+
24
+ # Define a foreign key constraint. Valid options are :on_update,
25
+ # :on_delete, and :deferrable, with values as described at
26
+ # ConnectionAdapters::ForeignKeyDefinition
27
+ #
28
+ # (NOTE: Sqlite3 does not support altering a table to add foreign-key
29
+ # constraints; they must be included in the table specification when
30
+ # it's created. If you're using Sqlite3, this method will raise an
31
+ # error.)
32
+ def add_foreign_key(table_name, column_names, references_table_name, references_column_names, options = {})
33
+ connection.add_foreign_key(table_name, column_names, references_table_name, references_column_names, options)
34
+ end
35
+
36
+ # Remove a foreign key constraint
37
+ #
38
+ # (NOTE: Sqlite3 does not support altering a table to remove
39
+ # foreign-key constraints. If you're using Sqlite3, this method will
40
+ # raise an error.)
41
+ def remove_foreign_key(table_name, foreign_key_name)
42
+ connection.remove_foreign_key(table_name, foreign_key_name)
43
+ end
44
+
45
+ # Enhances ActiveRecord::Migration#add_column to support indexes and foreign keys, with automatic creation
46
+ #
47
+ # == Indexes
48
+ #
49
+ # The <tt>:index</tt> option takes a hash of parameters to pass to ActiveRecord::Migration.add_index. Thus
50
+ #
51
+ # add_column('books', 'isbn', :string, :index => {:name => "ISBN-index", :unique => true })
52
+ #
53
+ # is equivalent to:
54
+ #
55
+ # add_column('books', 'isbn', :string)
56
+ # add_index('books', ['isbn'], :name => "ISBN-index", :unique => true)
57
+ #
58
+ #
59
+ # In order to support multi-column indexes, an special parameter <tt>:with</tt> may be specified, which takes another column name or an array of column names to include in the index. Thus
60
+ #
61
+ # add_column('contacts', 'phone_number', :string, :index => { :with => [:country_code, :area_code], :unique => true })
62
+ #
63
+ # is equivalent to:
64
+ #
65
+ # add_column('contacts', 'phone_number', :string)
66
+ # add_index('contacts', ['phone_number', 'country_code', 'area_code'], :unique => true)
67
+ #
68
+ #
69
+ # Some convenient shorthands are available:
70
+ #
71
+ # add_column('books', 'isbn', :index => true) # adds index with no extra options
72
+ # add_column('books', 'isbn', :index => :unique) # adds index with :unique => true
73
+ #
74
+ # == Foreign Key Constraints
75
+ #
76
+ # The +:references+ option takes the name of a table to reference in
77
+ # a foreign key constraint. For example:
78
+ #
79
+ # add_column('widgets', 'color', :integer, :references => 'colors')
80
+ #
81
+ # is equivalent to
82
+ #
83
+ # add_column('widgets', 'color', :integer)
84
+ # add_foreign_key('widgets', 'color', 'colors', 'id')
85
+ #
86
+ # The foreign column name defaults to +id+, but a different column
87
+ # can be specified using <tt>:references => [table_name,column_name]</tt>
88
+ #
89
+ # Additional options +:on_update+ and +:on_delete+ can be spcified,
90
+ # with values as described at ConnectionAdapters::ForeignKeyDefinition. For example:
91
+ #
92
+ # add_column('comments', 'post', :integer, :references => 'posts', :on_delete => :cascade)
93
+ #
94
+ # Global default values for +:on_update+ and +:on_delete+ can be
95
+ # specified in SchemaPlus.steup via, e.g., <tt>config.foreign_keys.on_update = :cascade</tt>
96
+ #
97
+ # == Automatic Foreign Key Constraints
98
+ #
99
+ # SchemaPlus supports the convention of naming foreign key columns
100
+ # with a suffix of +_id+. That is, if you define a column suffixed
101
+ # with +_id+, SchemaPlus assumes an implied :references to a table
102
+ # whose name is the column name prefix, pluralized. For example,
103
+ # these are equivalent:
104
+ #
105
+ # add_column('posts', 'author_id', :integer)
106
+ # add_column('posts', 'author_id', :integer, :references => 'authors')
107
+ #
108
+ # As a special case, if the column is named 'parent_id', SchemaPlus
109
+ # assumes it's a self reference, for a record that acts as a node of
110
+ # a tree. Thus, these are equivalent:
111
+ #
112
+ # add_column('sections', 'parent_id', :integer)
113
+ # add_column('sections', 'parent_id', :integer, :references => 'sections')
114
+ #
115
+ # If the implicit +:references+ value isn't what you want (e.g., the
116
+ # table name isn't pluralized), you can explicitly specify
117
+ # +:references+ and it will override the implicit value.
118
+ #
119
+ # If you don't want a foreign key constraint to be created, specify
120
+ # <tt>:references => nil</tt>.
121
+ # To disable automatic foreign key constraint creation globally, set
122
+ # <tt>config.foreign_keys.auto_create = false</tt> in
123
+ # SchemaPlus.steup.
124
+ #
125
+ # == Automatic Foreign Key Indexes
126
+ #
127
+ # Since efficient use of foreign key constraints requires that the
128
+ # referencing column be indexed, SchemaPlus will automatically create
129
+ # an index for the column if it created a foreign key. Thus
130
+ #
131
+ # add_column('widgets', 'color', :integer, :references => 'colors')
132
+ #
133
+ # is equivalent to:
134
+ #
135
+ # add_column('widgets', 'color', :integer, :references => 'colors', :index => true)
136
+ #
137
+ # If you want to pass options to the index, you can explcitly pass
138
+ # index options, such as <tt>:index => :unique</tt>.
139
+ #
140
+ # If you don't want an index to be created, specify
141
+ # <tt>:index => nil</tt>.
142
+ # To disable automatic foreign key index creation globally, set
143
+ # <tt>config.foreign_keys.auto_index = false</tt> in
144
+ # SchemaPlus.steup. (*Note*: If you're using MySQL, it will
145
+ # automatically create an index for foreign keys if you don't.)
146
+ #
147
+ def add_column(table_name, column_name, type, options = {})
148
+ super
149
+ handle_column_options(table_name, column_name, options)
150
+ end
151
+
152
+ # Enhances ActiveRecord::Migration#change_column to support indexes and foreign keys same as add_column.
153
+ def change_column(table_name, column_name, type, options = {})
154
+ super
155
+ remove_foreign_key_if_exists(table_name, column_name)
156
+ handle_column_options(table_name, column_name, options)
157
+ end
158
+
159
+ # Determines referenced table and column.
160
+ # Used in migrations.
161
+ #
162
+ # If auto_create is true:
163
+ # get_references('comments', 'post_id') # => ['posts', 'id']
164
+ #
165
+ # And if <tt>column_name</tt> is parent_id it references to the same table
166
+ # get_references('pages', 'parent_id') # => ['pages', 'id']
167
+ #
168
+ # If :references option is given, it is used (whether or not auto_create is true)
169
+ # get_references('widgets', 'main_page_id', :references => 'pages'))
170
+ # # => ['pages', 'id']
171
+ #
172
+ # Also the referenced id column may be specified:
173
+ # get_references('addresses', 'member_id', :references => ['users', 'uuid'])
174
+ # # => ['users', 'uuid']
175
+ def get_references(table_name, column_name, options = {}, config=nil) #:nodoc:
176
+ column_name = column_name.to_s
177
+ if options.has_key?(:references)
178
+ references = options[:references]
179
+ references = [references, :id] unless references.nil? || references.is_a?(Array)
180
+ references
181
+ elsif (config || SchemaPlus.config).foreign_keys.auto_create? && !ActiveRecord::Schema.defining?
182
+ if column_name == 'parent_id'
183
+ [table_name, :id]
184
+ elsif column_name =~ /^(.*)_id$/
185
+ determined_table_name = ActiveRecord::Base.pluralize_table_names ? $1.to_s.pluralize : $1
186
+ [determined_table_name, :id]
187
+ end
188
+ end
189
+ end
190
+
191
+ protected
192
+ def handle_column_options(table_name, column_name, options) #:nodoc:
193
+ if references = get_references(table_name, column_name, options)
194
+ if index = options.fetch(:index, SchemaPlus.config.foreign_keys.auto_index? && !ActiveRecord::Schema.defining?)
195
+ column_index(table_name, column_name, index)
196
+ end
197
+ add_foreign_key(table_name, column_name, references.first, references.last,
198
+ options.reverse_merge(:on_update => SchemaPlus.config.foreign_keys.on_update,
199
+ :on_delete => SchemaPlus.config.foreign_keys.on_delete))
200
+ elsif options[:index]
201
+ column_index(table_name, column_name, options[:index])
202
+ end
203
+ end
204
+
205
+ def column_index(table_name, column_name, options) #:nodoc:
206
+ options = {} if options == true
207
+ options = { :unique => true } if options == :unique
208
+ column_name = [column_name] + Array.wrap(options.delete(:with)).compact
209
+ add_index(table_name, column_name, options)
210
+ end
211
+
212
+ def remove_foreign_key_if_exists(table_name, column_name) #:nodoc:
213
+ foreign_keys = ActiveRecord::Base.connection.foreign_keys(table_name.to_s)
214
+ fk = foreign_keys.detect { |fk| fk.table_name == table_name.to_s && fk.column_names == Array(column_name).collect(&:to_s) }
215
+ remove_foreign_key(table_name, fk.name) if fk
216
+ end
217
+
218
+ end
219
+ end
220
+ end