Merge pull request #20005 from kamipo/default_expression_support

Add `:expression` option support on the schema default
This commit is contained in:
Rafael França 2016-01-16 04:18:21 -02:00
commit de2259791c
11 changed files with 100 additions and 44 deletions

@ -1,3 +1,13 @@
* Add expression support on the schema default.
Example:
create_table :posts do |t|
t.datetime :published_at, default: -> { 'NOW()' }
end
*Ryuta Kamizono*
* Fix regression when loading fixture files with symbol keys. * Fix regression when loading fixture files with symbol keys.
Fixes #22584. Fixes #22584.

@ -102,9 +102,13 @@ def quote_table_name_for_assignment(table, attr)
quote_table_name("#{table}.#{attr}") quote_table_name("#{table}.#{attr}")
end end
def quote_default_expression(value, column) #:nodoc: def quote_default_expression(value, column) # :nodoc:
value = lookup_cast_type(column.sql_type).serialize(value) if value.is_a?(Proc)
quote(value) value.call
else
value = lookup_cast_type(column.sql_type).serialize(value)
quote(value)
end
end end
def quoted_true def quoted_true

@ -76,11 +76,17 @@ def schema_scale(column)
def schema_default(column) def schema_default(column)
type = lookup_cast_type_from_column(column) type = lookup_cast_type_from_column(column)
default = type.deserialize(column.default) default = type.deserialize(column.default)
unless default.nil? if default.nil?
schema_expression(column)
else
type.type_cast_for_schema(default) type.type_cast_for_schema(default)
end end
end end
def schema_expression(column)
"-> { #{column.default_function.inspect} }" if column.default_function
end
def schema_collation(column) def schema_collation(column)
column.collation.inspect if column.collation column.collation.inspect if column.collation
end end

@ -495,12 +495,16 @@ def indexes(table_name, name = nil) #:nodoc:
end end
# Returns an array of +Column+ objects for the table specified by +table_name+. # Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name)#:nodoc: def columns(table_name) # :nodoc:
sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}" sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
execute_and_free(sql, 'SCHEMA') do |result| execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field| each_hash(result).map do |field|
type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
new_column(field[:Field], field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP"
new_column(field[:Field], nil, type_metadata, field[:Null] == "YES", field[:Default], field[:Collation])
else
new_column(field[:Field], field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation])
end
end end
end end
end end

@ -55,10 +55,11 @@ def quoted_date(value) #:nodoc:
end end
end end
# Does not quote function default values for UUID columns def quote_default_expression(value, column) # :nodoc:
def quote_default_expression(value, column) #:nodoc: if value.is_a?(Proc)
if column.type == :uuid && value =~ /\(\)/ value.call
value elsif column.type == :uuid && value =~ /\(\)/
value # Does not quote function default values for UUID columns
elsif column.respond_to?(:array?) elsif column.respond_to?(:array?)
value = type_cast_from_column(column, value) value = type_cast_from_column(column, value)
quote(value) quote(value)

@ -9,7 +9,7 @@ def column_spec_for_primary_key(column)
spec[:id] = ':bigserial' spec[:id] = ':bigserial'
elsif column.type == :uuid elsif column.type == :uuid
spec[:id] = ':uuid' spec[:id] = ':uuid'
spec[:default] = column.default_function.inspect spec[:default] = schema_default(column) || 'nil'
else else
spec[:id] = column.type.inspect spec[:id] = column.type.inspect
spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) })
@ -41,12 +41,8 @@ def schema_type(column)
end end
end end
def schema_default(column) def schema_expression(column)
if column.default_function super unless column.serial?
column.default_function.inspect unless column.serial?
else
super
end
end end
end end
end end

@ -512,8 +512,13 @@ def extract_limit(sql_type) # :nodoc:
def extract_value_from_default(default) # :nodoc: def extract_value_from_default(default) # :nodoc:
case default case default
# Quoted types # Quoted types
when /\A[\(B]?'(.*)'::/m when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
$1.gsub("''".freeze, "'".freeze) # The default 'now'::date is CURRENT_DATE
if $1 == "now".freeze && $2 == "date".freeze
nil
else
$1.gsub("''".freeze, "'".freeze)
end
# Boolean types # Boolean types
when 'true'.freeze, 'false'.freeze when 'true'.freeze, 'false'.freeze
default default
@ -535,7 +540,7 @@ def extract_default_function(default_value, default) # :nodoc:
end end
def has_default_function?(default_value, default) # :nodoc: def has_default_function?(default_value, default) # :nodoc:
!default_value && (%r{\w+\(.*\)} === default) !default_value && (%r{\w+\(.*\)|\(.*\)::\w+} === default)
end end
def load_additional_types(type_map, oids = nil) # :nodoc: def load_additional_types(type_map, oids = nil) # :nodoc:

@ -197,14 +197,14 @@ def test_pk_and_sequence_for_uuid_primary_key
def test_schema_dumper_for_uuid_primary_key def test_schema_dumper_for_uuid_primary_key
schema = dump_table_schema "pg_uuids" schema = dump_table_schema "pg_uuids"
assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema) assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema)
assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema) assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema)
end end
def test_schema_dumper_for_uuid_primary_key_with_custom_default def test_schema_dumper_for_uuid_primary_key_with_custom_default
schema = dump_table_schema "pg_uuids_2" schema = dump_table_schema "pg_uuids_2"
assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema) assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema)
assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema) assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema)
end end
end end
end end

@ -1,4 +1,5 @@
require "cases/helper" require "cases/helper"
require 'support/schema_dumping_helper'
require 'models/default' require 'models/default'
require 'models/entrant' require 'models/entrant'
@ -80,7 +81,32 @@ def test_default_strings_containing_single_quotes
end end
end end
if current_adapter?(:PostgreSQLAdapter)
class PostgresqlDefaultExpressionTest < ActiveRecord::TestCase
include SchemaDumpingHelper
test "schema dump includes default expression" do
output = dump_table_schema("defaults")
assert_match %r/t\.date\s+"modified_date",\s+default: -> { "\('now'::text\)::date" }/, output
assert_match %r/t\.date\s+"modified_date_function",\s+default: -> { "now\(\)" }/, output
assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "now\(\)" }/, output
assert_match %r/t\.datetime\s+"modified_time_function",\s+default: -> { "now\(\)" }/, output
end
end
end
if current_adapter?(:Mysql2Adapter) if current_adapter?(:Mysql2Adapter)
class MysqlDefaultExpressionTest < ActiveRecord::TestCase
include SchemaDumpingHelper
if ActiveRecord::Base.connection.version >= '5.6.0'
test "schema dump includes default expression" do
output = dump_table_schema("datetime_defaults")
assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output
end
end
end
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase
# ActiveRecord::Base#create! (and #save and other related methods) will # ActiveRecord::Base#create! (and #save and other related methods) will
# open a new transaction. When in transactional tests mode, this will # open a new transaction. When in transactional tests mode, this will

@ -1,4 +1,11 @@
ActiveRecord::Schema.define do ActiveRecord::Schema.define do
if ActiveRecord::Base.connection.version >= '5.6.0'
create_table :datetime_defaults, force: true do |t|
t.datetime :modified_datetime, default: -> { 'CURRENT_TIMESTAMP' }
end
end
create_table :binary_fields, force: true do |t| create_table :binary_fields, force: true do |t|
t.binary :var_binary, limit: 255 t.binary :var_binary, limit: 255
t.binary :var_binary_large, limit: 4095 t.binary :var_binary_large, limit: 4095

@ -11,7 +11,23 @@
t.uuid :uuid_parent_id t.uuid :uuid_parent_id
end end
%w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones create_table :defaults, force: true do |t|
t.date :modified_date, default: -> { 'CURRENT_DATE' }
t.date :modified_date_function, default: -> { 'now()' }
t.date :fixed_date, default: '2004-01-01'
t.datetime :modified_time, default: -> { 'CURRENT_TIMESTAMP' }
t.datetime :modified_time_function, default: -> { 'now()' }
t.datetime :fixed_time, default: '2004-01-01 00:00:00.000000-00'
t.column :char1, 'char(1)', default: 'Y'
t.string :char2, limit: 50, default: 'a varchar field'
t.text :char3, default: 'a text field'
t.bigint :bigint_default, default: -> { '0::bigint' }
t.text :multiline_default, default: '--- []
'
end
%w(postgresql_times postgresql_oids postgresql_timestamp_with_zones
postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name|
drop_table table_name, if_exists: true drop_table table_name, if_exists: true
end end
@ -27,25 +43,6 @@
execute "SELECT setval('#{seq_name}', 100)" execute "SELECT setval('#{seq_name}', 100)"
end end
execute <<_SQL
CREATE TABLE defaults (
id serial primary key,
modified_date date default CURRENT_DATE,
modified_date_function date default now(),
fixed_date date default '2004-01-01',
modified_time timestamp default CURRENT_TIMESTAMP,
modified_time_function timestamp default now(),
fixed_time timestamp default '2004-01-01 00:00:00.000000-00',
char1 char(1) default 'Y',
char2 character varying(50) default 'a varchar field',
char3 text default 'a text field',
bigint_default bigint default 0::bigint,
multiline_default text DEFAULT '--- []
'::text
);
_SQL
execute <<_SQL execute <<_SQL
CREATE TABLE postgresql_times ( CREATE TABLE postgresql_times (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,