mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add basic support for check constraints to database migrations
This commit is contained in:
parent
aaf20e3c7e
commit
1944a7e74c
20 changed files with 459 additions and 12 deletions
|
@ -1,3 +1,14 @@
|
|||
* Add basic support for CHECK constraints to database migrations.
|
||||
|
||||
Usage:
|
||||
|
||||
```ruby
|
||||
add_check_constraint :products, "price > 0", name: "price_check"
|
||||
remove_check_constraint :products, name: "price_check"
|
||||
```
|
||||
|
||||
*fatkodima*
|
||||
|
||||
* Add `ActiveRecord::Base.strict_loading_by_default` and `ActiveRecord::Base.strict_loading_by_default=`
|
||||
to enable/disable strict_loading mode by default for a model. The configuration's value is
|
||||
inheritable by subclasses, but they can override that value and it will not impact parent class.
|
||||
|
|
|
@ -17,6 +17,7 @@ module ActiveRecord
|
|||
autoload :ColumnDefinition
|
||||
autoload :ChangeColumnDefinition
|
||||
autoload :ForeignKeyDefinition
|
||||
autoload :CheckConstraintDefinition
|
||||
autoload :TableDefinition
|
||||
autoload :Table
|
||||
autoload :AlterTable
|
||||
|
|
|
@ -15,7 +15,8 @@ module ActiveRecord
|
|||
|
||||
delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
|
||||
:options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options,
|
||||
:quoted_columns_for_index, :supports_partial_index?, to: :@conn, private: true
|
||||
:quoted_columns_for_index, :supports_partial_index?, :supports_check_constraints?, :check_constraint_options,
|
||||
to: :@conn, private: true
|
||||
|
||||
private
|
||||
def visit_AlterTable(o)
|
||||
|
@ -23,6 +24,8 @@ module ActiveRecord
|
|||
sql << o.adds.map { |col| accept col }.join(" ")
|
||||
sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(" ")
|
||||
sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(" ")
|
||||
sql << o.check_constraint_adds.map { |con| visit_AddCheckConstraint con }.join(" ")
|
||||
sql << o.check_constraint_drops.map { |con| visit_DropCheckConstraint con }.join(" ")
|
||||
end
|
||||
|
||||
def visit_ColumnDefinition(o)
|
||||
|
@ -52,6 +55,10 @@ module ActiveRecord
|
|||
statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
|
||||
end
|
||||
|
||||
if supports_check_constraints?
|
||||
statements.concat(o.check_constraints.map { |expression, options| check_constraint_in_create(o.name, expression, options) })
|
||||
end
|
||||
|
||||
create_sql << "(#{statements.join(', ')})" if statements.present?
|
||||
add_table_options!(create_sql, o)
|
||||
create_sql << " AS #{to_sql(o.as)}" if o.as
|
||||
|
@ -98,6 +105,18 @@ module ActiveRecord
|
|||
sql.join(" ")
|
||||
end
|
||||
|
||||
def visit_CheckConstraintDefinition(o)
|
||||
"CONSTRAINT #{o.name} CHECK (#{o.expression})"
|
||||
end
|
||||
|
||||
def visit_AddCheckConstraint(o)
|
||||
"ADD #{accept(o)}"
|
||||
end
|
||||
|
||||
def visit_DropCheckConstraint(name)
|
||||
"DROP CONSTRAINT #{quote_column_name(name)}"
|
||||
end
|
||||
|
||||
def quoted_columns(o)
|
||||
String === o.columns ? o.columns : quoted_columns_for_index(o.columns, o.column_options)
|
||||
end
|
||||
|
@ -148,6 +167,11 @@ module ActiveRecord
|
|||
accept ForeignKeyDefinition.new(from_table, to_table, options)
|
||||
end
|
||||
|
||||
def check_constraint_in_create(table_name, expression, options)
|
||||
options = check_constraint_options(table_name, expression, options)
|
||||
accept CheckConstraintDefinition.new(table_name, expression, options)
|
||||
end
|
||||
|
||||
def action_sql(action, dependency)
|
||||
case dependency
|
||||
when :nullify then "ON #{action} SET NULL"
|
||||
|
|
|
@ -127,6 +127,16 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
CheckConstraintDefinition = Struct.new(:table_name, :expression, :options) do
|
||||
def name
|
||||
options[:name]
|
||||
end
|
||||
|
||||
def export_name_on_schema_dump?
|
||||
!ActiveRecord::SchemaDumper.chk_ignore_pattern.match?(name) if name
|
||||
end
|
||||
end
|
||||
|
||||
class ReferenceDefinition # :nodoc:
|
||||
def initialize(
|
||||
name,
|
||||
|
@ -267,7 +277,7 @@ module ActiveRecord
|
|||
class TableDefinition
|
||||
include ColumnMethods
|
||||
|
||||
attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys
|
||||
attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys, :check_constraints
|
||||
|
||||
def initialize(
|
||||
conn,
|
||||
|
@ -284,6 +294,7 @@ module ActiveRecord
|
|||
@indexes = []
|
||||
@foreign_keys = []
|
||||
@primary_keys = nil
|
||||
@check_constraints = []
|
||||
@temporary = temporary
|
||||
@if_not_exists = if_not_exists
|
||||
@options = options
|
||||
|
@ -412,6 +423,10 @@ module ActiveRecord
|
|||
foreign_keys << [table_name, options]
|
||||
end
|
||||
|
||||
def check_constraint(expression, **options)
|
||||
check_constraints << [expression, 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]
|
||||
#
|
||||
|
@ -471,14 +486,16 @@ module ActiveRecord
|
|||
|
||||
class AlterTable # :nodoc:
|
||||
attr_reader :adds
|
||||
attr_reader :foreign_key_adds
|
||||
attr_reader :foreign_key_drops
|
||||
attr_reader :foreign_key_adds, :foreign_key_drops
|
||||
attr_reader :check_constraint_adds, :check_constraint_drops
|
||||
|
||||
def initialize(td)
|
||||
@td = td
|
||||
@adds = []
|
||||
@foreign_key_adds = []
|
||||
@foreign_key_drops = []
|
||||
@check_constraint_adds = []
|
||||
@check_constraint_drops = []
|
||||
end
|
||||
|
||||
def name; @td.name; end
|
||||
|
@ -491,6 +508,14 @@ module ActiveRecord
|
|||
@foreign_key_drops << name
|
||||
end
|
||||
|
||||
def add_check_constraint(expression, options)
|
||||
@check_constraint_adds << CheckConstraintDefinition.new(name, expression, options)
|
||||
end
|
||||
|
||||
def drop_check_constraint(constraint_name)
|
||||
@check_constraint_drops << constraint_name
|
||||
end
|
||||
|
||||
def add_column(name, type, **options)
|
||||
name = name.to_s
|
||||
type = type.to_sym
|
||||
|
@ -515,6 +540,7 @@ module ActiveRecord
|
|||
# t.rename
|
||||
# t.references
|
||||
# t.belongs_to
|
||||
# t.check_constraint
|
||||
# t.string
|
||||
# t.text
|
||||
# t.integer
|
||||
|
@ -536,6 +562,7 @@ module ActiveRecord
|
|||
# t.remove_references
|
||||
# t.remove_belongs_to
|
||||
# t.remove_index
|
||||
# t.remove_check_constraint
|
||||
# t.remove_timestamps
|
||||
# end
|
||||
#
|
||||
|
@ -737,6 +764,24 @@ module ActiveRecord
|
|||
def foreign_key_exists?(*args, **options)
|
||||
@base.foreign_key_exists?(name, *args, **options)
|
||||
end
|
||||
|
||||
# Adds a check constraint.
|
||||
#
|
||||
# t.check_constraint("price > 0", name: "price_check")
|
||||
#
|
||||
# See {connection.add_check_constraint}[rdoc-ref:SchemaStatements#add_check_constraint]
|
||||
def check_constraint(*args)
|
||||
@base.add_check_constraint(name, *args)
|
||||
end
|
||||
|
||||
# Removes the given check constraint from the table.
|
||||
#
|
||||
# t.remove_check_constraint(name: "price_check")
|
||||
#
|
||||
# See {connection.remove_check_constraint}[rdoc-ref:SchemaStatements#remove_check_constraint]
|
||||
def remove_check_constraint(*args)
|
||||
@base.remove_check_constraint(name, *args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1128,6 +1128,55 @@ module ActiveRecord
|
|||
options
|
||||
end
|
||||
|
||||
# Returns an array of check constraints for the given table.
|
||||
# The check constraints are represented as CheckConstraintDefinition objects.
|
||||
def check_constraints(table_name)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Adds a new check constraint to the table. +expression+ is a String
|
||||
# representation of verifiable boolean condition.
|
||||
#
|
||||
# add_check_constraint :products, "price > 0", name: "price_check"
|
||||
#
|
||||
# generates:
|
||||
#
|
||||
# ALTER TABLE "products" ADD CONSTRAINT price_check CHECK (price > 0)
|
||||
#
|
||||
def add_check_constraint(table_name, expression, **options)
|
||||
return unless supports_check_constraints?
|
||||
|
||||
options = check_constraint_options(table_name, expression, options)
|
||||
at = create_alter_table(table_name)
|
||||
at.add_check_constraint(expression, options)
|
||||
|
||||
execute schema_creation.accept(at)
|
||||
end
|
||||
|
||||
def check_constraint_options(table_name, expression, options) # :nodoc:
|
||||
options = options.dup
|
||||
options[:name] ||= check_constraint_name(table_name, expression: expression, **options)
|
||||
options
|
||||
end
|
||||
|
||||
# Removes the given check constraint from the table.
|
||||
#
|
||||
# remove_check_constraint :products, name: "price_check"
|
||||
#
|
||||
# The +expression+ parameter will be ignored if present. It can be helpful
|
||||
# to provide this in a migration's +change+ method so it can be reverted.
|
||||
# In that case, +expression+ will be used by #add_check_constraint.
|
||||
def remove_check_constraint(table_name, expression = nil, **options)
|
||||
return unless supports_check_constraints?
|
||||
|
||||
chk_name_to_delete = check_constraint_for!(table_name, expression: expression, **options).name
|
||||
|
||||
at = create_alter_table(table_name)
|
||||
at.drop_check_constraint(chk_name_to_delete)
|
||||
|
||||
execute schema_creation.accept(at)
|
||||
end
|
||||
|
||||
def dump_schema_information # :nodoc:
|
||||
versions = schema_migration.all_versions
|
||||
insert_versions_sql(versions) if versions.any?
|
||||
|
@ -1457,6 +1506,27 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def check_constraint_name(table_name, **options)
|
||||
options.fetch(:name) do
|
||||
expression = options.fetch(:expression)
|
||||
identifier = "#{table_name}_#{expression}_chk"
|
||||
hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
|
||||
|
||||
"chk_rails_#{hashed_identifier}"
|
||||
end
|
||||
end
|
||||
|
||||
def check_constraint_for(table_name, **options)
|
||||
return unless supports_check_constraints?
|
||||
chk_name = check_constraint_name(table_name, **options)
|
||||
check_constraints(table_name).detect { |chk| chk.name == chk_name }
|
||||
end
|
||||
|
||||
def check_constraint_for!(table_name, expression: nil, **options)
|
||||
check_constraint_for(table_name, expression: expression, **options) ||
|
||||
raise(ArgumentError, "Table '#{table_name}' has no check constraint for #{expression || options}")
|
||||
end
|
||||
|
||||
def validate_index_length!(table_name, new_name, internal = false)
|
||||
if new_name.length > index_name_length
|
||||
raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
|
||||
|
|
|
@ -338,6 +338,11 @@ module ActiveRecord
|
|||
end
|
||||
deprecate :supports_foreign_keys_in_create?
|
||||
|
||||
# Does this adapter support creating check constraints?
|
||||
def supports_check_constraints?
|
||||
false
|
||||
end
|
||||
|
||||
# Does this adapter support views?
|
||||
def supports_views?
|
||||
false
|
||||
|
|
|
@ -92,6 +92,14 @@ module ActiveRecord
|
|||
true
|
||||
end
|
||||
|
||||
def supports_check_constraints?
|
||||
if mariadb?
|
||||
database_version >= "10.2.1"
|
||||
else
|
||||
database_version >= "8.0.16"
|
||||
end
|
||||
end
|
||||
|
||||
def supports_views?
|
||||
true
|
||||
end
|
||||
|
@ -415,6 +423,30 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def check_constraints(table_name)
|
||||
scope = quoted_scope(table_name)
|
||||
|
||||
chk_info = exec_query(<<~SQL, "SCHEMA")
|
||||
SELECT cc.constraint_name AS 'name',
|
||||
cc.check_clause AS 'expression'
|
||||
FROM information_schema.check_constraints cc
|
||||
JOIN information_schema.table_constraints tc
|
||||
USING (constraint_schema, constraint_name)
|
||||
WHERE tc.table_schema = #{scope[:schema]}
|
||||
AND tc.table_name = #{scope[:name]}
|
||||
AND cc.constraint_schema = #{scope[:schema]}
|
||||
SQL
|
||||
|
||||
chk_info.map do |row|
|
||||
options = {
|
||||
name: row["name"]
|
||||
}
|
||||
expression = row["expression"]
|
||||
expression = expression[1..-2] unless mariadb? # remove parentheses added by mysql
|
||||
CheckConstraintDefinition.new(table_name, expression, options)
|
||||
end
|
||||
end
|
||||
|
||||
def table_options(table_name) # :nodoc:
|
||||
create_table_info = create_table_info(table_name)
|
||||
|
||||
|
|
|
@ -11,6 +11,10 @@ module ActiveRecord
|
|||
"DROP FOREIGN KEY #{name}"
|
||||
end
|
||||
|
||||
def visit_DropCheckConstraint(name)
|
||||
"DROP #{mariadb? ? 'CONSTRAINT' : 'CHECK'} #{name}"
|
||||
end
|
||||
|
||||
def visit_AddColumnDefinition(o)
|
||||
add_column_position!(super, column_options(o.column))
|
||||
end
|
||||
|
|
|
@ -519,6 +519,27 @@ module ActiveRecord
|
|||
query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present?
|
||||
end
|
||||
|
||||
def check_constraints(table_name) # :nodoc:
|
||||
scope = quoted_scope(table_name)
|
||||
|
||||
check_info = exec_query(<<-SQL, "SCHEMA")
|
||||
SELECT conname, pg_get_constraintdef(c.oid) AS constraintdef
|
||||
FROM pg_constraint c
|
||||
JOIN pg_class t ON c.conrelid = t.oid
|
||||
WHERE c.contype = 'c'
|
||||
AND t.relname = #{scope[:name]}
|
||||
SQL
|
||||
|
||||
check_info.map do |row|
|
||||
options = {
|
||||
name: row["conname"]
|
||||
}
|
||||
expression = row["constraintdef"][/CHECK \({2}(.+)\){2}/, 1]
|
||||
|
||||
CheckConstraintDefinition.new(table_name, expression, options)
|
||||
end
|
||||
end
|
||||
|
||||
# Maps logical Rails types to PostgreSQL-specific data types.
|
||||
def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc:
|
||||
sql = \
|
||||
|
|
|
@ -166,6 +166,10 @@ module ActiveRecord
|
|||
true
|
||||
end
|
||||
|
||||
def supports_check_constraints?
|
||||
true
|
||||
end
|
||||
|
||||
def supports_validate_constraints?
|
||||
true
|
||||
end
|
||||
|
|
|
@ -78,6 +78,35 @@ module ActiveRecord
|
|||
alter_table(from_table, foreign_keys)
|
||||
end
|
||||
|
||||
def check_constraints(table_name)
|
||||
table_sql = query_value(<<-SQL, "SCHEMA")
|
||||
SELECT sql
|
||||
FROM sqlite_master
|
||||
WHERE name = #{quote_table_name(table_name)} AND type = 'table'
|
||||
UNION ALL
|
||||
SELECT sql
|
||||
FROM sqlite_temp_master
|
||||
WHERE name = #{quote_table_name(table_name)} AND type = 'table'
|
||||
SQL
|
||||
|
||||
table_sql.scan(/CONSTRAINT\s+(?<name>\w+)\s+CHECK\s+\((?<expression>(:?[^()]|\(\g<expression>\))+)\)/i).map do |name, expression|
|
||||
CheckConstraintDefinition.new(table_name, expression, name: name)
|
||||
end
|
||||
end
|
||||
|
||||
def add_check_constraint(table_name, expression, **options)
|
||||
alter_table(table_name) do |definition|
|
||||
definition.check_constraint(expression, **options)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_check_constraint(table_name, expression = nil, **options)
|
||||
check_constraints = check_constraints(table_name)
|
||||
chk_name_to_delete = check_constraint_for!(table_name, expression: expression, **options).name
|
||||
check_constraints.delete_if { |chk| chk.name == chk_name_to_delete }
|
||||
alter_table(table_name, foreign_keys(table_name), check_constraints)
|
||||
end
|
||||
|
||||
def create_schema_dumper(options)
|
||||
SQLite3::SchemaDumper.create(self, options)
|
||||
end
|
||||
|
|
|
@ -136,6 +136,10 @@ module ActiveRecord
|
|||
true
|
||||
end
|
||||
|
||||
def supports_check_constraints?
|
||||
true
|
||||
end
|
||||
|
||||
def supports_views?
|
||||
true
|
||||
end
|
||||
|
@ -361,7 +365,12 @@ module ActiveRecord
|
|||
options[:null] == false && options[:default].nil?
|
||||
end
|
||||
|
||||
def alter_table(table_name, foreign_keys = foreign_keys(table_name), **options)
|
||||
def alter_table(
|
||||
table_name,
|
||||
foreign_keys = foreign_keys(table_name),
|
||||
check_constraints = check_constraints(table_name),
|
||||
**options
|
||||
)
|
||||
altered_table_name = "a#{table_name}"
|
||||
|
||||
caller = lambda do |definition|
|
||||
|
@ -374,6 +383,10 @@ module ActiveRecord
|
|||
definition.foreign_key(to_table, **fk.options)
|
||||
end
|
||||
|
||||
check_constraints.each do |chk|
|
||||
definition.check_constraint(chk.expression, **chk.options)
|
||||
end
|
||||
|
||||
yield definition if block_given?
|
||||
end
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ module ActiveRecord
|
|||
#
|
||||
# * add_column
|
||||
# * add_foreign_key
|
||||
# * add_check_constraint
|
||||
# * add_index
|
||||
# * add_reference
|
||||
# * add_timestamps
|
||||
|
@ -25,6 +26,7 @@ module ActiveRecord
|
|||
# * remove_column (must supply a type)
|
||||
# * remove_columns (must specify at least one column name or more)
|
||||
# * remove_foreign_key (must supply a second table)
|
||||
# * remove_check_constraint
|
||||
# * remove_index
|
||||
# * remove_reference
|
||||
# * remove_timestamps
|
||||
|
@ -39,7 +41,8 @@ module ActiveRecord
|
|||
:drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension,
|
||||
:change_column, :execute, :remove_columns, :change_column_null,
|
||||
:add_foreign_key, :remove_foreign_key,
|
||||
:change_column_comment, :change_table_comment
|
||||
:change_column_comment, :change_table_comment,
|
||||
:add_check_constraint, :remove_check_constraint
|
||||
]
|
||||
include JoinTable
|
||||
|
||||
|
@ -136,6 +139,7 @@ module ActiveRecord
|
|||
add_timestamps: :remove_timestamps,
|
||||
add_reference: :remove_reference,
|
||||
add_foreign_key: :remove_foreign_key,
|
||||
add_check_constraint: :remove_check_constraint,
|
||||
enable_extension: :disable_extension
|
||||
}.each do |cmd, inv|
|
||||
[[inv, cmd], [cmd, inv]].uniq.each do |method, inverse|
|
||||
|
@ -265,6 +269,11 @@ module ActiveRecord
|
|||
[:change_table_comment, [table, from: options[:to], to: options[:from]]]
|
||||
end
|
||||
|
||||
def invert_remove_check_constraint(args)
|
||||
raise ActiveRecord::IrreversibleMigration, "remove_check_constraint is only reversible if given an expression." if args.size < 2
|
||||
super
|
||||
end
|
||||
|
||||
def respond_to_missing?(method, _)
|
||||
super || delegate.respond_to?(method)
|
||||
end
|
||||
|
|
|
@ -23,6 +23,12 @@ module ActiveRecord
|
|||
# should not be dumped to db/schema.rb.
|
||||
cattr_accessor :fk_ignore_pattern, default: /^fk_rails_[0-9a-f]{10}$/
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Specify a custom regular expression matching check constraints which name
|
||||
# should not be dumped to db/schema.rb.
|
||||
cattr_accessor :chk_ignore_pattern, default: /^chk_rails_[0-9a-f]{10}$/
|
||||
|
||||
class << self
|
||||
def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base)
|
||||
connection.create_schema_dumper(generate_options(config)).dump(stream)
|
||||
|
@ -159,6 +165,7 @@ HEADER
|
|||
end
|
||||
|
||||
indexes_in_create(table, tbl)
|
||||
check_constraints_in_create(table, tbl) if @connection.supports_check_constraints?
|
||||
|
||||
tbl.puts " end"
|
||||
tbl.puts
|
||||
|
@ -212,6 +219,24 @@ HEADER
|
|||
index_parts
|
||||
end
|
||||
|
||||
def check_constraints_in_create(table, stream)
|
||||
if (check_constraints = @connection.check_constraints(table)).any?
|
||||
add_check_constraint_statements = check_constraints.map do |check_constraint|
|
||||
parts = [
|
||||
"t.check_constraint #{check_constraint.expression.inspect}"
|
||||
]
|
||||
|
||||
if check_constraint.export_name_on_schema_dump?
|
||||
parts << "name: #{check_constraint.name.inspect}"
|
||||
end
|
||||
|
||||
" #{parts.join(', ')}"
|
||||
end
|
||||
|
||||
stream.puts add_check_constraint_statements.sort.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
def foreign_keys(table, stream)
|
||||
if (foreign_keys = @connection.foreign_keys(table)).any?
|
||||
add_foreign_key_statements = foreign_keys.map do |foreign_key|
|
||||
|
|
|
@ -345,6 +345,20 @@ module ActiveRecord
|
|||
assert_equal :delete_me, t.name
|
||||
end
|
||||
end
|
||||
|
||||
def test_check_constraint_creates_check_constraint
|
||||
with_change_table do |t|
|
||||
@connection.expect :add_check_constraint, nil, [:delete_me, "price > discounted_price", name: "price_check"]
|
||||
t.check_constraint "price > discounted_price", name: "price_check"
|
||||
end
|
||||
end
|
||||
|
||||
def test_remove_check_constraint_removes_check_constraint
|
||||
with_change_table do |t|
|
||||
@connection.expect :remove_check_constraint, nil, [:delete_me, name: "price_check"]
|
||||
t.remove_check_constraint name: "price_check"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
118
activerecord/test/cases/migration/check_constraint_test.rb
Normal file
118
activerecord/test/cases/migration/check_constraint_test.rb
Normal file
|
@ -0,0 +1,118 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "cases/helper"
|
||||
require "support/schema_dumping_helper"
|
||||
|
||||
if ActiveRecord::Base.connection.supports_check_constraints?
|
||||
module ActiveRecord
|
||||
class Migration
|
||||
class CheckConstraintTest < ActiveRecord::TestCase
|
||||
include SchemaDumpingHelper
|
||||
|
||||
class Trade < ActiveRecord::Base
|
||||
end
|
||||
|
||||
setup do
|
||||
@connection = ActiveRecord::Base.connection
|
||||
@connection.create_table "trades", force: true do |t|
|
||||
t.integer :price
|
||||
t.integer :quantity
|
||||
end
|
||||
end
|
||||
|
||||
teardown do
|
||||
@connection.drop_table "trades", if_exists: true
|
||||
end
|
||||
|
||||
def test_check_constraints
|
||||
check_constraints = @connection.check_constraints("products")
|
||||
assert_equal 1, check_constraints.size
|
||||
|
||||
constraint = check_constraints.first
|
||||
assert_equal "products", constraint.table_name
|
||||
assert_equal "products_price_check", constraint.name
|
||||
|
||||
if current_adapter?(:Mysql2Adapter)
|
||||
assert_equal "`price` > `discounted_price`", constraint.expression
|
||||
else
|
||||
assert_equal "price > discounted_price", constraint.expression
|
||||
end
|
||||
end
|
||||
|
||||
def test_add_check_constraint
|
||||
@connection.add_check_constraint :trades, "quantity > 0"
|
||||
|
||||
check_constraints = @connection.check_constraints("trades")
|
||||
assert_equal 1, check_constraints.size
|
||||
|
||||
constraint = check_constraints.first
|
||||
assert_equal "trades", constraint.table_name
|
||||
assert_equal "chk_rails_2189e9f96c", constraint.name
|
||||
|
||||
if current_adapter?(:Mysql2Adapter)
|
||||
assert_equal "`quantity` > 0", constraint.expression
|
||||
else
|
||||
assert_equal "quantity > 0", constraint.expression
|
||||
end
|
||||
end
|
||||
|
||||
def test_added_check_constraint_ensures_valid_values
|
||||
@connection.add_check_constraint :trades, "quantity > 0", name: "quantity_check"
|
||||
|
||||
assert_raises(ActiveRecord::StatementInvalid) do
|
||||
Trade.create(quantity: -1)
|
||||
end
|
||||
end
|
||||
|
||||
def test_remove_check_constraint
|
||||
@connection.add_check_constraint :trades, "price > 0", name: "price_check"
|
||||
@connection.add_check_constraint :trades, "quantity > 0", name: "quantity_check"
|
||||
|
||||
assert_equal 2, @connection.check_constraints("trades").size
|
||||
@connection.remove_check_constraint :trades, name: "quantity_check"
|
||||
assert_equal 1, @connection.check_constraints("trades").size
|
||||
|
||||
constraint = @connection.check_constraints("trades").first
|
||||
assert_equal "trades", constraint.table_name
|
||||
assert_equal "price_check", constraint.name
|
||||
|
||||
if current_adapter?(:Mysql2Adapter)
|
||||
assert_equal "`price` > 0", constraint.expression
|
||||
else
|
||||
assert_equal "price > 0", constraint.expression
|
||||
end
|
||||
end
|
||||
|
||||
def test_remove_non_existing_check_constraint
|
||||
assert_raises(ArgumentError) do
|
||||
@connection.remove_check_constraint :trades, name: "nonexistent"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
module ActiveRecord
|
||||
class Migration
|
||||
class NoForeignKeySupportTest < ActiveRecord::TestCase
|
||||
setup do
|
||||
@connection = ActiveRecord::Base.connection
|
||||
end
|
||||
|
||||
def test_add_check_constraint_should_be_noop
|
||||
@connection.add_check_constraint :products, "discounted_price > 0", name: "discounted_price_check"
|
||||
end
|
||||
|
||||
def test_remove_check_constraint_should_be_noop
|
||||
@connection.remove_check_constraint :products, name: "price_check"
|
||||
end
|
||||
|
||||
def test_check_constraints_should_raise_not_implemented
|
||||
assert_raises(NotImplementedError) do
|
||||
@connection.check_constraints("products")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -421,6 +421,17 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_invert_remove_check_constraint
|
||||
enable = @recorder.inverse_of :remove_check_constraint, [:dogs, "speed > 0", name: "speed_check"]
|
||||
assert_equal [:add_check_constraint, [:dogs, "speed > 0", name: "speed_check"], nil], enable
|
||||
end
|
||||
|
||||
def test_invert_remove_check_constraint_without_expression
|
||||
assert_raises(ActiveRecord::IrreversibleMigration) do
|
||||
@recorder.inverse_of :remove_check_constraint, [:dogs]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -200,6 +200,17 @@ class SchemaDumperTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
if ActiveRecord::Base.connection.supports_check_constraints?
|
||||
def test_schema_dumps_check_constraints
|
||||
constraint_definition = dump_table_schema("products").split(/\n/).grep(/t.check_constraint.*products_price_check/).first.strip
|
||||
if current_adapter?(:Mysql2Adapter)
|
||||
assert_equal 't.check_constraint "`price` > `discounted_price`", name: "products_price_check"', constraint_definition
|
||||
else
|
||||
assert_equal 't.check_constraint "price > discounted_price", name: "products_price_check"', constraint_definition
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_schema_dump_should_honor_nonstandard_primary_keys
|
||||
output = standard_dump
|
||||
match = output.match(%r{create_table "movies"(.*)do})
|
||||
|
|
|
@ -767,9 +767,13 @@ ActiveRecord::Schema.define do
|
|||
create_table :products, force: true do |t|
|
||||
t.references :collection
|
||||
t.references :type
|
||||
t.string :name
|
||||
t.string :name
|
||||
t.decimal :price
|
||||
t.decimal :discounted_price
|
||||
end
|
||||
|
||||
add_check_constraint :products, "price > discounted_price", name: "products_price_check"
|
||||
|
||||
create_table :product_types, force: true do |t|
|
||||
t.string :name
|
||||
end
|
||||
|
|
|
@ -725,10 +725,6 @@ of `create_table` and `reversible`, replacing `create_table`
|
|||
by `drop_table`, and finally replacing `up` by `down` and vice-versa.
|
||||
This is all taken care of by `revert`.
|
||||
|
||||
NOTE: If you want to add check constraints like in the examples above,
|
||||
you will have to use `structure.sql` as dump method. See
|
||||
[Schema Dumping and You](#schema-dumping-and-you).
|
||||
|
||||
Running Migrations
|
||||
------------------
|
||||
|
||||
|
@ -970,7 +966,7 @@ database and expressing its structure using `create_table`, `add_index`, and so
|
|||
on.
|
||||
|
||||
`db/schema.rb` cannot express everything your database may support such as
|
||||
triggers, sequences, stored procedures, check constraints, etc. While migrations
|
||||
triggers, sequences, stored procedures, etc. While migrations
|
||||
may use `execute` to create database constructs that are not supported by the
|
||||
Ruby migration DSL, these constructs may not be able to be reconstituted by the
|
||||
schema dumper. If you are using features like these, you should set the schema
|
||||
|
|
Loading…
Reference in a new issue