module SchemaPlus::ActiveRecord
# SchemaPlus extends ActiveRecord::Migration with several enhancements. See documentation at Migration::ClassMethods
module Migration
def self.included(base) #:nodoc:
base.extend(ClassMethods)
end
#
# SchemaPlus extends ActiveRecord::Migration with the following enhancements.
#
module ClassMethods
# Create a new view, given its name and SQL definition
#
def create_view(view_name, definition)
connection.create_view(view_name, definition)
end
# Drop the named view
def drop_view(view_name)
connection.drop_view(view_name)
end
# Define a foreign key constraint. Valid options are :on_update,
# :on_delete, and :deferrable, with values as described at
# ConnectionAdapters::ForeignKeyDefinition
#
# (NOTE: Sqlite3 does not support altering a table to add foreign-key
# constraints; they must be included in the table specification when
# it's created. If you're using Sqlite3, this method will raise an
# error.)
def add_foreign_key(table_name, column_names, references_table_name, references_column_names, options = {})
connection.add_foreign_key(table_name, column_names, references_table_name, references_column_names, options)
end
# Remove a foreign key constraint
#
# (NOTE: Sqlite3 does not support altering a table to remove
# foreign-key constraints. If you're using Sqlite3, this method will
# raise an error.)
def remove_foreign_key(table_name, foreign_key_name)
connection.remove_foreign_key(table_name, foreign_key_name)
end
# Enhances ActiveRecord::Migration#add_column to support indexes and foreign keys, with automatic creation
#
# == Indexes
#
# The :index option takes a hash of parameters to pass to ActiveRecord::Migration.add_index. Thus
#
# add_column('books', 'isbn', :string, :index => {:name => "ISBN-index", :unique => true })
#
# is equivalent to:
#
# add_column('books', 'isbn', :string)
# add_index('books', ['isbn'], :name => "ISBN-index", :unique => true)
#
#
# In order to support multi-column indexes, an special parameter :with may be specified, which takes another column name or an array of column names to include in the index. Thus
#
# add_column('contacts', 'phone_number', :string, :index => { :with => [:country_code, :area_code], :unique => true })
#
# is equivalent to:
#
# add_column('contacts', 'phone_number', :string)
# add_index('contacts', ['phone_number', 'country_code', 'area_code'], :unique => true)
#
#
# Some convenient shorthands are available:
#
# add_column('books', 'isbn', :index => true) # adds index with no extra options
# add_column('books', 'isbn', :index => :unique) # adds index with :unique => true
#
# == Foreign Key Constraints
#
# The +:references+ option takes the name of a table to reference in
# a foreign key constraint. For example:
#
# add_column('widgets', 'color', :integer, :references => 'colors')
#
# is equivalent to
#
# add_column('widgets', 'color', :integer)
# add_foreign_key('widgets', 'color', 'colors', 'id')
#
# The foreign column name defaults to +id+, but a different column
# can be specified using :references => [table_name,column_name]
#
# Additional options +:on_update+ and +:on_delete+ can be spcified,
# with values as described at ConnectionAdapters::ForeignKeyDefinition. For example:
#
# add_column('comments', 'post', :integer, :references => 'posts', :on_delete => :cascade)
#
# Global default values for +:on_update+ and +:on_delete+ can be
# specified in SchemaPlus.steup via, e.g., config.foreign_keys.on_update = :cascade
#
# == Automatic Foreign Key Constraints
#
# SchemaPlus supports the convention of naming foreign key columns
# with a suffix of +_id+. That is, if you define a column suffixed
# with +_id+, SchemaPlus assumes an implied :references to a table
# whose name is the column name prefix, pluralized. For example,
# these are equivalent:
#
# add_column('posts', 'author_id', :integer)
# add_column('posts', 'author_id', :integer, :references => 'authors')
#
# As a special case, if the column is named 'parent_id', SchemaPlus
# assumes it's a self reference, for a record that acts as a node of
# a tree. Thus, these are equivalent:
#
# add_column('sections', 'parent_id', :integer)
# add_column('sections', 'parent_id', :integer, :references => 'sections')
#
# If the implicit +:references+ value isn't what you want (e.g., the
# table name isn't pluralized), you can explicitly specify
# +:references+ and it will override the implicit value.
#
# If you don't want a foreign key constraint to be created, specify
# :references => nil.
# To disable automatic foreign key constraint creation globally, set
# config.foreign_keys.auto_create = false in
# SchemaPlus.steup.
#
# == Automatic Foreign Key Indexes
#
# Since efficient use of foreign key constraints requires that the
# referencing column be indexed, SchemaPlus will automatically create
# an index for the column if it created a foreign key. Thus
#
# add_column('widgets', 'color', :integer, :references => 'colors')
#
# is equivalent to:
#
# add_column('widgets', 'color', :integer, :references => 'colors', :index => true)
#
# If you want to pass options to the index, you can explcitly pass
# index options, such as :index => :unique.
#
# If you don't want an index to be created, specify
# :index => nil.
# To disable automatic foreign key index creation globally, set
# config.foreign_keys.auto_index = false in
# SchemaPlus.steup. (*Note*: If you're using MySQL, it will
# automatically create an index for foreign keys if you don't.)
#
def add_column(table_name, column_name, type, options = {})
super
handle_column_options(table_name, column_name, options)
end
# Enhances ActiveRecord::Migration#change_column to support indexes and foreign keys same as add_column.
def change_column(table_name, column_name, type, options = {})
super
remove_foreign_key_if_exists(table_name, column_name)
handle_column_options(table_name, column_name, options)
end
# Determines referenced table and column.
# Used in migrations.
#
# If auto_create is true:
# get_references('comments', 'post_id') # => ['posts', 'id']
#
# And if column_name is parent_id it references to the same table
# get_references('pages', 'parent_id') # => ['pages', 'id']
#
# If :references option is given, it is used (whether or not auto_create is true)
# get_references('widgets', 'main_page_id', :references => 'pages'))
# # => ['pages', 'id']
#
# Also the referenced id column may be specified:
# get_references('addresses', 'member_id', :references => ['users', 'uuid'])
# # => ['users', 'uuid']
def get_references(table_name, column_name, options = {}, config=nil) #:nodoc:
column_name = column_name.to_s
if options.has_key?(:references)
references = options[:references]
references = [references, :id] unless references.nil? || references.is_a?(Array)
references
elsif (config || SchemaPlus.config).foreign_keys.auto_create? && !ActiveRecord::Schema.defining?
if column_name == 'parent_id'
[table_name, :id]
elsif column_name =~ /^(.*)_id$/
determined_table_name = ActiveRecord::Base.pluralize_table_names ? $1.to_s.pluralize : $1
[determined_table_name, :id]
end
end
end
protected
def handle_column_options(table_name, column_name, options) #:nodoc:
if references = get_references(table_name, column_name, options)
if index = options.fetch(:index, SchemaPlus.config.foreign_keys.auto_index? && !ActiveRecord::Schema.defining?)
column_index(table_name, column_name, index)
end
add_foreign_key(table_name, column_name, references.first, references.last,
options.reverse_merge(:on_update => SchemaPlus.config.foreign_keys.on_update,
:on_delete => SchemaPlus.config.foreign_keys.on_delete))
elsif options[:index]
column_index(table_name, column_name, options[:index])
end
end
def column_index(table_name, column_name, options) #:nodoc:
options = {} if options == true
options = { :unique => true } if options == :unique
column_name = [column_name] + Array.wrap(options.delete(:with)).compact
add_index(table_name, column_name, options)
end
def remove_foreign_key_if_exists(table_name, column_name) #:nodoc:
foreign_keys = ActiveRecord::Base.connection.foreign_keys(table_name.to_s)
fk = foreign_keys.detect { |fk| fk.table_name == table_name.to_s && fk.column_names == Array(column_name).collect(&:to_s) }
remove_foreign_key(table_name, fk.name) if fk
end
end
end
end