schema_plus 0.1.0.pre1

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.
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