aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb')
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb702
1 files changed, 702 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
new file mode 100644
index 0000000000..db489143af
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -0,0 +1,702 @@
+# frozen_string_literal: true
+
+require "active_support/deprecation"
+
+module ActiveRecord
+ module ConnectionAdapters #:nodoc:
+ # Abstract representation of an index definition on a table. Instances of
+ # this type are typically created and returned by methods in database
+ # adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes
+ class IndexDefinition # :nodoc:
+ attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :comment
+
+ def initialize(
+ table, name,
+ unique = false,
+ columns = [],
+ lengths: {},
+ orders: {},
+ opclasses: {},
+ where: nil,
+ type: nil,
+ using: nil,
+ comment: nil
+ )
+ @table = table
+ @name = name
+ @unique = unique
+ @columns = columns
+ @lengths = concise_options(lengths)
+ @orders = concise_options(orders)
+ @opclasses = concise_options(opclasses)
+ @where = where
+ @type = type
+ @using = using
+ @comment = comment
+ end
+
+ private
+ def concise_options(options)
+ if columns.size == options.size && options.values.uniq.size == 1
+ options.values.first
+ else
+ options
+ end
+ end
+ end
+
+ # Abstract representation of a column definition. Instances of this type
+ # are typically created by methods in TableDefinition, and added to the
+ # +columns+ attribute of said TableDefinition object, in order to be used
+ # for generating a number of table creation or table changing SQL statements.
+ ColumnDefinition = Struct.new(:name, :type, :options, :sql_type) do # :nodoc:
+ def primary_key?
+ options[:primary_key]
+ end
+
+ [:limit, :precision, :scale, :default, :null, :collation, :comment].each do |option_name|
+ module_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{option_name}
+ options[:#{option_name}]
+ end
+
+ def #{option_name}=(value)
+ options[:#{option_name}] = value
+ end
+ CODE
+ end
+ end
+
+ AddColumnDefinition = Struct.new(:column) # :nodoc:
+
+ ChangeColumnDefinition = Struct.new(:column, :name) #:nodoc:
+
+ PrimaryKeyDefinition = Struct.new(:name) # :nodoc:
+
+ ForeignKeyDefinition = Struct.new(:from_table, :to_table, :options) do #:nodoc:
+ def name
+ options[:name]
+ end
+
+ def column
+ options[:column]
+ end
+
+ def primary_key
+ options[:primary_key] || default_primary_key
+ end
+
+ def on_delete
+ options[:on_delete]
+ end
+
+ def on_update
+ options[:on_update]
+ end
+
+ def custom_primary_key?
+ options[:primary_key] != default_primary_key
+ end
+
+ def validate?
+ options.fetch(:validate, true)
+ end
+ alias validated? validate?
+
+ def export_name_on_schema_dump?
+ name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern
+ end
+
+ def defined_for?(to_table_ord = nil, to_table: nil, **options)
+ if to_table_ord
+ self.to_table == to_table_ord.to_s
+ else
+ (to_table.nil? || to_table.to_s == self.to_table) &&
+ options.all? { |k, v| self.options[k].to_s == v.to_s }
+ end
+ end
+
+ private
+ def default_primary_key
+ "id"
+ end
+ end
+
+ class ReferenceDefinition # :nodoc:
+ def initialize(
+ name,
+ polymorphic: false,
+ index: true,
+ foreign_key: false,
+ type: :bigint,
+ **options
+ )
+ @name = name
+ @polymorphic = polymorphic
+ @index = index
+ @foreign_key = foreign_key
+ @type = type
+ @options = options
+
+ if polymorphic && foreign_key
+ raise ArgumentError, "Cannot add a foreign key to a polymorphic relation"
+ end
+ end
+
+ def add_to(table)
+ columns.each do |column_options|
+ table.column(*column_options)
+ end
+
+ if index
+ table.index(column_names, index_options)
+ end
+
+ if foreign_key
+ table.foreign_key(foreign_table_name, foreign_key_options)
+ end
+ end
+
+ private
+ attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options
+
+ def as_options(value)
+ value.is_a?(Hash) ? value : {}
+ end
+
+ def polymorphic_options
+ as_options(polymorphic).merge(options.slice(:null, :first, :after))
+ end
+
+ def index_options
+ as_options(index)
+ end
+
+ def foreign_key_options
+ as_options(foreign_key).merge(column: column_name)
+ end
+
+ def columns
+ result = [[column_name, type, options]]
+ if polymorphic
+ result.unshift(["#{name}_type", :string, polymorphic_options])
+ end
+ result
+ end
+
+ def column_name
+ "#{name}_id"
+ end
+
+ def column_names
+ columns.map(&:first)
+ end
+
+ def foreign_table_name
+ foreign_key_options.fetch(:to_table) do
+ Base.pluralize_table_names ? name.to_s.pluralize : name
+ end
+ end
+ end
+
+ module ColumnMethods
+ # Appends a primary key definition to the table definition.
+ # Can be called multiple times, but this is probably not a good idea.
+ def primary_key(name, type = :primary_key, **options)
+ column(name, type, options.merge(primary_key: true))
+ end
+
+ # Appends a column or columns of a specified type.
+ #
+ # t.string(:goat)
+ # t.string(:goat, :sheep)
+ #
+ # See TableDefinition#column
+ [
+ :bigint,
+ :binary,
+ :boolean,
+ :date,
+ :datetime,
+ :decimal,
+ :float,
+ :integer,
+ :json,
+ :string,
+ :text,
+ :time,
+ :timestamp,
+ :virtual,
+ ].each do |column_type|
+ module_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{column_type}(*args, **options)
+ args.each { |name| column(name, :#{column_type}, options) }
+ end
+ CODE
+ end
+ alias_method :numeric, :decimal
+ end
+
+ # Represents the schema of an SQL table in an abstract way. This class
+ # provides methods for manipulating the schema representation.
+ #
+ # Inside migration files, the +t+ object in {create_table}[rdoc-ref:SchemaStatements#create_table]
+ # is actually of this type:
+ #
+ # class SomeMigration < ActiveRecord::Migration[5.0]
+ # def up
+ # create_table :foo do |t|
+ # puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
+ # end
+ # end
+ #
+ # def down
+ # ...
+ # end
+ # end
+ #
+ class TableDefinition
+ include ColumnMethods
+
+ attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys
+ attr_writer :indexes
+ deprecate :indexes=
+
+ def initialize(
+ name,
+ temporary: false,
+ if_not_exists: false,
+ options: nil,
+ as: nil,
+ comment: nil,
+ **
+ )
+ @columns_hash = {}
+ @indexes = []
+ @foreign_keys = []
+ @primary_keys = nil
+ @temporary = temporary
+ @if_not_exists = if_not_exists
+ @options = options
+ @as = as
+ @name = name
+ @comment = comment
+ end
+
+ def primary_keys(name = nil) # :nodoc:
+ @primary_keys = PrimaryKeyDefinition.new(name) if name
+ @primary_keys
+ end
+
+ # Returns an array of ColumnDefinition objects for the columns of the table.
+ def columns; @columns_hash.values; end
+
+ # Returns a ColumnDefinition for the column with name +name+.
+ def [](name)
+ @columns_hash[name.to_s]
+ end
+
+ # Instantiates a new column for the table.
+ # See {connection.add_column}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_column]
+ # for available options.
+ #
+ # Additional options are:
+ # * <tt>:index</tt> -
+ # Create an index for the column. Can be either <tt>true</tt> or an options hash.
+ #
+ # This method returns <tt>self</tt>.
+ #
+ # == Examples
+ #
+ # # Assuming +td+ is an instance of TableDefinition
+ # td.column(:granted, :boolean, index: true)
+ #
+ # == Short-hand examples
+ #
+ # Instead of calling #column directly, you can also work with the short-hand definitions for the default types.
+ # They use the type as the method name instead of as a parameter and allow for multiple columns to be defined
+ # in a single statement.
+ #
+ # What can be written like this with the regular calls to column:
+ #
+ # create_table :products do |t|
+ # t.column :shop_id, :integer
+ # t.column :creator_id, :integer
+ # t.column :item_number, :string
+ # t.column :name, :string, default: "Untitled"
+ # t.column :value, :string, default: "Untitled"
+ # t.column :created_at, :datetime
+ # t.column :updated_at, :datetime
+ # end
+ # add_index :products, :item_number
+ #
+ # can also be written as follows using the short-hand:
+ #
+ # create_table :products do |t|
+ # t.integer :shop_id, :creator_id
+ # t.string :item_number, index: true
+ # t.string :name, :value, default: "Untitled"
+ # t.timestamps null: false
+ # end
+ #
+ # There's a short-hand method for each of the type values declared at the top. And then there's
+ # TableDefinition#timestamps that'll add +created_at+ and +updated_at+ as datetimes.
+ #
+ # TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type
+ # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of
+ # options, these will be used when creating the <tt>_type</tt> column. The <tt>:index</tt> option
+ # will also create an index, similar to calling {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index].
+ # So what can be written like this:
+ #
+ # create_table :taggings do |t|
+ # t.integer :tag_id, :tagger_id, :taggable_id
+ # t.string :tagger_type
+ # t.string :taggable_type, default: 'Photo'
+ # end
+ # add_index :taggings, :tag_id, name: 'index_taggings_on_tag_id'
+ # add_index :taggings, [:tagger_id, :tagger_type]
+ #
+ # Can also be written as follows using references:
+ #
+ # create_table :taggings do |t|
+ # t.references :tag, index: { name: 'index_taggings_on_tag_id' }
+ # t.references :tagger, polymorphic: true
+ # t.references :taggable, polymorphic: { default: 'Photo' }, index: false
+ # end
+ def column(name, type, options = {})
+ name = name.to_s
+ type = type.to_sym if type
+ options = options.dup
+
+ if @columns_hash[name]
+ if @columns_hash[name].primary_key?
+ raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
+ else
+ raise ArgumentError, "you can't define an already defined column '#{name}'."
+ end
+ end
+
+ index_options = options.delete(:index)
+ index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options
+ @columns_hash[name] = new_column_definition(name, type, options)
+ self
+ end
+
+ # remove the column +name+ from the table.
+ # remove_column(:account_id)
+ def remove_column(name)
+ @columns_hash.delete name.to_s
+ end
+
+ # Adds index options to the indexes hash, keyed by column name
+ # This is primarily used to track indexes that need to be created after the table
+ #
+ # index(:account_id, name: 'index_projects_on_account_id')
+ def index(column_name, options = {})
+ indexes << [column_name, options]
+ end
+
+ def foreign_key(table_name, options = {}) # :nodoc:
+ table_name_prefix = ActiveRecord::Base.table_name_prefix
+ table_name_suffix = ActiveRecord::Base.table_name_suffix
+ table_name = "#{table_name_prefix}#{table_name}#{table_name_suffix}"
+ foreign_keys.push([table_name, options])
+ end
+
+ # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
+ # <tt>:updated_at</tt> to the table. See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps]
+ #
+ # t.timestamps null: false
+ def timestamps(**options)
+ options[:null] = false if options[:null].nil?
+
+ column(:created_at, :datetime, options)
+ column(:updated_at, :datetime, options)
+ end
+
+ # Adds a reference.
+ #
+ # t.references(:user)
+ # t.belongs_to(:supplier, foreign_key: true)
+ #
+ # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
+ def references(*args, **options)
+ args.each do |ref_name|
+ ReferenceDefinition.new(ref_name, options).add_to(self)
+ end
+ end
+ alias :belongs_to :references
+
+ def new_column_definition(name, type, **options) # :nodoc:
+ if integer_like_primary_key?(type, options)
+ type = integer_like_primary_key_type(type, options)
+ end
+ type = aliased_types(type.to_s, type)
+ options[:primary_key] ||= type == :primary_key
+ options[:null] = false if options[:primary_key]
+ create_column_definition(name, type, options)
+ end
+
+ private
+ def create_column_definition(name, type, options)
+ ColumnDefinition.new(name, type, options)
+ end
+
+ def aliased_types(name, fallback)
+ "timestamp" == name ? :datetime : fallback
+ end
+
+ def integer_like_primary_key?(type, options)
+ options[:primary_key] && [:integer, :bigint].include?(type) && !options.key?(:default)
+ end
+
+ def integer_like_primary_key_type(type, options)
+ type
+ end
+ end
+
+ class AlterTable # :nodoc:
+ attr_reader :adds
+ attr_reader :foreign_key_adds
+ attr_reader :foreign_key_drops
+
+ def initialize(td)
+ @td = td
+ @adds = []
+ @foreign_key_adds = []
+ @foreign_key_drops = []
+ end
+
+ def name; @td.name; end
+
+ def add_foreign_key(to_table, options)
+ @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options)
+ end
+
+ def drop_foreign_key(name)
+ @foreign_key_drops << name
+ end
+
+ def add_column(name, type, options)
+ name = name.to_s
+ type = type.to_sym
+ @adds << AddColumnDefinition.new(@td.new_column_definition(name, type, options))
+ end
+ end
+
+ # Represents an SQL table in an abstract way for updating a table.
+ # Also see TableDefinition and {connection.create_table}[rdoc-ref:SchemaStatements#create_table]
+ #
+ # Available transformations are:
+ #
+ # change_table :table do |t|
+ # t.primary_key
+ # t.column
+ # t.index
+ # t.rename_index
+ # t.timestamps
+ # t.change
+ # t.change_default
+ # t.rename
+ # t.references
+ # t.belongs_to
+ # t.string
+ # t.text
+ # t.integer
+ # t.bigint
+ # t.float
+ # t.decimal
+ # t.numeric
+ # t.datetime
+ # t.timestamp
+ # t.time
+ # t.date
+ # t.binary
+ # t.boolean
+ # t.foreign_key
+ # t.json
+ # t.virtual
+ # t.remove
+ # t.remove_references
+ # t.remove_belongs_to
+ # t.remove_index
+ # t.remove_timestamps
+ # end
+ #
+ class Table
+ include ColumnMethods
+
+ attr_reader :name
+
+ def initialize(table_name, base)
+ @name = table_name
+ @base = base
+ end
+
+ # Adds a new column to the named table.
+ #
+ # t.column(:name, :string)
+ #
+ # See TableDefinition#column for details of the options you can use.
+ def column(column_name, type, options = {})
+ index_options = options.delete(:index)
+ @base.add_column(name, column_name, type, options)
+ index(column_name, index_options.is_a?(Hash) ? index_options : {}) if index_options
+ end
+
+ # Checks to see if a column exists.
+ #
+ # t.string(:name) unless t.column_exists?(:name, :string)
+ #
+ # See {connection.column_exists?}[rdoc-ref:SchemaStatements#column_exists?]
+ def column_exists?(column_name, type = nil, options = {})
+ @base.column_exists?(name, column_name, type, options)
+ end
+
+ # Adds a new index to the table. +column_name+ can be a single Symbol, or
+ # an Array of Symbols.
+ #
+ # t.index(:name)
+ # t.index([:branch_id, :party_id], unique: true)
+ # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party')
+ #
+ # See {connection.add_index}[rdoc-ref:SchemaStatements#add_index] for details of the options you can use.
+ def index(column_name, options = {})
+ @base.add_index(name, column_name, options)
+ end
+
+ # Checks to see if an index exists.
+ #
+ # unless t.index_exists?(:branch_id)
+ # t.index(:branch_id)
+ # end
+ #
+ # See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?]
+ def index_exists?(column_name, options = {})
+ @base.index_exists?(name, column_name, options)
+ end
+
+ # Renames the given index on the table.
+ #
+ # t.rename_index(:user_id, :account_id)
+ #
+ # See {connection.rename_index}[rdoc-ref:SchemaStatements#rename_index]
+ def rename_index(index_name, new_index_name)
+ @base.rename_index(name, index_name, new_index_name)
+ end
+
+ # Adds timestamps (+created_at+ and +updated_at+) columns to the table.
+ #
+ # t.timestamps(null: false)
+ #
+ # See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps]
+ def timestamps(options = {})
+ @base.add_timestamps(name, options)
+ end
+
+ # Changes the column's definition according to the new options.
+ #
+ # t.change(:name, :string, limit: 80)
+ # t.change(:description, :text)
+ #
+ # See TableDefinition#column for details of the options you can use.
+ def change(column_name, type, options = {})
+ @base.change_column(name, column_name, type, options)
+ end
+
+ # Sets a new default value for a column.
+ #
+ # t.change_default(:qualification, 'new')
+ # t.change_default(:authorized, 1)
+ # t.change_default(:status, from: nil, to: "draft")
+ #
+ # See {connection.change_column_default}[rdoc-ref:SchemaStatements#change_column_default]
+ def change_default(column_name, default_or_changes)
+ @base.change_column_default(name, column_name, default_or_changes)
+ end
+
+ # Removes the column(s) from the table definition.
+ #
+ # t.remove(:qualification)
+ # t.remove(:qualification, :experience)
+ #
+ # See {connection.remove_columns}[rdoc-ref:SchemaStatements#remove_columns]
+ def remove(*column_names)
+ @base.remove_columns(name, *column_names)
+ end
+
+ # Removes the given index from the table.
+ #
+ # t.remove_index(:branch_id)
+ # t.remove_index(column: [:branch_id, :party_id])
+ # t.remove_index(name: :by_branch_party)
+ #
+ # See {connection.remove_index}[rdoc-ref:SchemaStatements#remove_index]
+ def remove_index(options = {})
+ @base.remove_index(name, options)
+ end
+
+ # Removes the timestamp columns (+created_at+ and +updated_at+) from the table.
+ #
+ # t.remove_timestamps
+ #
+ # See {connection.remove_timestamps}[rdoc-ref:SchemaStatements#remove_timestamps]
+ def remove_timestamps(options = {})
+ @base.remove_timestamps(name, options)
+ end
+
+ # Renames a column.
+ #
+ # t.rename(:description, :name)
+ #
+ # See {connection.rename_column}[rdoc-ref:SchemaStatements#rename_column]
+ def rename(column_name, new_column_name)
+ @base.rename_column(name, column_name, new_column_name)
+ end
+
+ # Adds a reference.
+ #
+ # t.references(:user)
+ # t.belongs_to(:supplier, foreign_key: true)
+ #
+ # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
+ def references(*args, **options)
+ args.each do |ref_name|
+ @base.add_reference(name, ref_name, options)
+ end
+ end
+ alias :belongs_to :references
+
+ # Removes a reference. Optionally removes a +type+ column.
+ #
+ # t.remove_references(:user)
+ # t.remove_belongs_to(:supplier, polymorphic: true)
+ #
+ # See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference]
+ def remove_references(*args, **options)
+ args.each do |ref_name|
+ @base.remove_reference(name, ref_name, options)
+ end
+ end
+ alias :remove_belongs_to :remove_references
+
+ # Adds a foreign key.
+ #
+ # t.foreign_key(:authors)
+ #
+ # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key]
+ def foreign_key(*args)
+ @base.add_foreign_key(name, *args)
+ end
+
+ # Checks to see if a foreign key exists.
+ #
+ # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors)
+ #
+ # See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?]
+ def foreign_key_exists?(*args)
+ @base.foreign_key_exists?(name, *args)
+ end
+ end
+ end
+end