diff options
14 files changed, 185 insertions, 138 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 2456376d6d..2c013a074a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -63,6 +63,17 @@ module ActiveRecord lookup_cast_type(column.sql_type) end + def fetch_type_metadata(sql_type) + cast_type = lookup_cast_type(sql_type) + SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + end + # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index b927cc4a8d..67c8f438e2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ require 'bigdecimal/util' require 'active_record/type' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' +require 'active_record/connection_adapters/sql_type_metadata' require 'active_record/connection_adapters/abstract/schema_dumper' require 'active_record/connection_adapters/abstract/schema_creation' require 'monitor' @@ -384,8 +385,8 @@ module ActiveRecord end end - def new_column(name, default, cast_type, sql_type = nil, null = true) - Column.new(name, default, cast_type, sql_type, null) + def new_column(name, default, sql_type_metadata = nil, null = true) + Column.new(name, default, sql_type_metadata, null) end def lookup_cast_type(sql_type) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index b61b717a61..5c8c4b883a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -74,13 +74,10 @@ module ActiveRecord end class Column < ConnectionAdapters::Column # :nodoc: - attr_reader :collation, :strict, :extra + delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true - def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "") - @strict = strict - @collation = collation - @extra = extra - super(name, default, cast_type, sql_type, null) + def initialize(*) + super assert_valid_default(default) extract_default end @@ -88,7 +85,7 @@ module ActiveRecord def extract_default if blob_or_text_column? @default = null || strict ? nil : '' - elsif missing_default_forged_as_empty_string?(@default) + elsif missing_default_forged_as_empty_string?(default) @default = nil end end @@ -106,13 +103,6 @@ module ActiveRecord collation && !collation.match(/_ci$/) end - def ==(other) - super && - collation == other.collation && - strict == other.strict && - extra == other.extra - end - private # MySQL misreports NOT NULL column default when none is given. @@ -131,9 +121,33 @@ module ActiveRecord raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" end end + end + + class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + attr_reader :collation, :extra, :strict + + def initialize(type_metadata, collation: "", extra: "", strict: false) + super(type_metadata) + @type_metadata = type_metadata + @collation = collation + @extra = extra + @strict = strict + end + + def ==(other) + other.is_a?(MysqlTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected def attributes_for_hash - super + [collation, strict, extra] + [self.class, @type_metadata, collation, extra, strict] end end @@ -243,8 +257,8 @@ module ActiveRecord raise NotImplementedError end - def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc: - Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) + def new_column(field, default, sql_type_metadata = nil, null = true) # :nodoc: + Column.new(field, default, sql_type_metadata, null) end # Must return the MySQL error number from the exception, if the exception has an @@ -467,8 +481,8 @@ module ActiveRecord each_hash(result).map do |field| field_name = set_field_encoding(field[:Field]) sql_type = field[:Type] - cast_type = lookup_cast_type(sql_type) - new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra]) + type_metadata = fetch_type_metadata(sql_type, field[:Collation], field[:Extra]) + new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES") end end end @@ -716,6 +730,10 @@ module ActiveRecord end end + def fetch_type_metadata(sql_type, collation = "", extra = "") + MysqlTypeMetadata.new(super(sql_type), collation: collation, extra: extra, strict: strict_mode?) + end + # MySQL is too stupid to create a temporary table for use subquery, so we have # to give it some prompting in the form of a subsubquery. Ugh! def subquery_for(key, select) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 077d953ca2..fa5ed07b8a 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -12,25 +12,21 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function + attr_reader :name, :null, :sql_type_metadata, :default, :default_function - delegate :precision, :scale, :limit, :type, to: :cast_type + delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true # Instantiates a new column in the table. # # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. - # +cast_type+ is the object used for type casting and type information. - # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in - # <tt>company_name varchar(60)</tt>. - # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. + # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) - @name = name - @cast_type = cast_type - @sql_type = sql_type - @null = null - @default = default + def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil) + @name = name + @sql_type_metadata = sql_type_metadata + @null = null + @default = default @default_function = default_function end @@ -47,12 +43,8 @@ module ActiveRecord end def ==(other) - other.name == name && - other.default == default && - other.cast_type == cast_type && - other.sql_type == sql_type && - other.null == null && - other.default_function == default_function + other.is_a?(Column) && + attributes_for_hash == other.attributes_for_hash end alias :eql? :== @@ -60,16 +52,16 @@ module ActiveRecord attributes_for_hash.hash end - private + protected def attributes_for_hash - [self.class, name, default, cast_type, sql_type, null, default_function] + [self.class, name, default, sql_type_metadata, null, default_function] end end class NullColumn < Column def initialize(name) - super name, nil, Type::Value.new + super(name, nil) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 8f3628ec0e..0eb4fb468c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -2,21 +2,9 @@ module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: - attr_reader :array, :oid, :fmod + delegate :array, :oid, :fmod, to: :sql_type_metadata alias :array? :array - def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil, oid = nil, fmod = nil) - if sql_type =~ /\[\]$/ - @array = true - sql_type = sql_type[0..sql_type.length - 3] - else - @array = false - end - @oid = oid - @fmod = fmod - super(name, default, cast_type, sql_type, null, default_function) - end - def serial? default_function && default_function =~ /\Anextval\(.*\)\z/ end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 519a9727fb..d4e183dd16 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -185,15 +185,15 @@ module ActiveRecord column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| oid = oid.to_i fmod = fmod.to_i - cast_type = get_oid_type(oid, fmod, column_name, type) - default_value = extract_value_from_default(cast_type, default) + type_metadata = fetch_type_metadata(column_name, type, oid, fmod) + default_value = extract_value_from_default(default) default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, cast_type, type, notnull == 'f', default_function, oid, fmod) + new_column(column_name, default_value, type_metadata, notnull == 'f', default_function) end end - def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil, oid = nil, fmod = nil) # :nodoc: - PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function, oid, fmod) + def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil) # :nodoc: + PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function) end # Returns the current database name. @@ -582,6 +582,18 @@ module ActiveRecord [super, *order_columns].join(', ') end + + def fetch_type_metadata(column_name, sql_type, oid, fmod) + cast_type = get_oid_type(oid, fmod, column_name, sql_type) + simple_type = SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb new file mode 100644 index 0000000000..58715978f7 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -0,0 +1,35 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) + attr_reader :oid, :fmod, :array + + def initialize(type_metadata, oid: nil, fmod: nil) + super(type_metadata) + @type_metadata = type_metadata + @oid = oid + @fmod = fmod + @array = /\[\]$/ === type_metadata.sql_type + end + + def sql_type + super.gsub(/\[\]$/, "") + end + + def ==(other) + other.is_a?(PostgreSQLTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, @type_metadata, oid, fmod] + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index d789069f0b..ab970e183a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,14 +1,14 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_record/connection_adapters/statement_pool' - -require 'active_record/connection_adapters/postgresql/utils' -require 'active_record/connection_adapters/postgresql/column' -require 'active_record/connection_adapters/postgresql/oid' -require 'active_record/connection_adapters/postgresql/quoting' -require 'active_record/connection_adapters/postgresql/referential_integrity' -require 'active_record/connection_adapters/postgresql/schema_definitions' -require 'active_record/connection_adapters/postgresql/schema_statements' -require 'active_record/connection_adapters/postgresql/database_statements' +require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/postgresql/column" +require "active_record/connection_adapters/postgresql/database_statements" +require "active_record/connection_adapters/postgresql/oid" +require "active_record/connection_adapters/postgresql/quoting" +require "active_record/connection_adapters/postgresql/referential_integrity" +require "active_record/connection_adapters/postgresql/schema_definitions" +require "active_record/connection_adapters/postgresql/schema_statements" +require "active_record/connection_adapters/postgresql/type_metadata" +require "active_record/connection_adapters/postgresql/utils" +require "active_record/connection_adapters/statement_pool" require 'arel/visitors/bind_visitor' @@ -541,7 +541,7 @@ module ActiveRecord end # Extracts the value from a PostgreSQL column default definition. - def extract_value_from_default(oid, default) # :nodoc: + def extract_value_from_default(default) # :nodoc: case default # Quoted types when /\A[\(B]?'(.*)'::/m diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb new file mode 100644 index 0000000000..ccb7e154ee --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb @@ -0,0 +1,32 @@ +module ActiveRecord + # :stopdoc: + module ConnectionAdapters + class SqlTypeMetadata + attr_reader :sql_type, :type, :limit, :precision, :scale + + def initialize(sql_type: nil, type: nil, limit: nil, precision: nil, scale: nil) + @sql_type = sql_type + @type = type + @limit = limit + @precision = precision + @scale = scale + end + + def ==(other) + other.is_a?(SqlTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, sql_type, type, limit, precision, scale] + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 02a3b65934..c06213a7bf 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -381,8 +381,8 @@ module ActiveRecord end sql_type = field['type'] - cast_type = lookup_cast_type(sql_type) - new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) + type_metadata = fetch_type_metadata(sql_type) + new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0) end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 293db1c57f..75adcccce6 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -312,7 +312,7 @@ module ActiveRecord @columns_hash.each do |name, column| define_attribute( name, - column.cast_type, + connection.lookup_cast_type_from_column(column), default: column.default, user_provided_default: false ) diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index ad9279aaca..49a68fb94c 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -29,8 +29,7 @@ module ActiveRecord @columns[table_name] << ActiveRecord::ConnectionAdapters::Column.new( name.to_s, options[:default], - lookup_cast_type(sql_type.to_s), - sql_type.to_s, + fetch_type_metadata(sql_type), options[:null]) end diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 68d5a232ba..1e38b97c4a 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -23,7 +23,7 @@ module ActiveRecord super @connection = ActiveRecord::Base.connection @subscriber = LogListener.new - @pk = ConnectionAdapters::Column.new(Topic.primary_key, nil, Topic.type_for_attribute(Topic.primary_key)) + @pk = Topic.columns_hash[Topic.primary_key] @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index bcfd66b4bf..14b95ecab1 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -14,7 +14,7 @@ module ActiveRecord # Avoid column definitions in create table statements like: # `title` varchar(255) DEFAULT NULL def test_should_not_include_default_clause_when_default_is_null - column = Column.new("title", nil, Type::String.new(limit: 20)) + column = Column.new("title", nil, SqlTypeMetadata.new(limit: 20)) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -22,7 +22,7 @@ module ActiveRecord end def test_should_include_default_clause_when_default_is_present - column = Column.new("title", "Hello", Type::String.new(limit: 20)) + column = Column.new("title", "Hello", SqlTypeMetadata.new(limit: 20)) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -30,94 +30,53 @@ module ActiveRecord end def test_should_specify_not_null_if_null_option_is_false - column = Column.new("title", "Hello", Type::String.new(limit: 20), "varchar(20)", false) + type_metadata = SqlTypeMetadata.new(limit: 20) + column = Column.new("title", "Hello", type_metadata, false) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, @viz.accept(column_def) end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_should_set_default_for_mysql_binary_data_types - binary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "binary(1)") + type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") + binary_column = AbstractMysqlAdapter::Column.new("title", "a", type) assert_equal "a", binary_column.default - varbinary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)") + type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") + varbinary_column = AbstractMysqlAdapter::Column.new("title", "a", type) assert_equal "a", varbinary_column.default end def test_should_not_set_default_for_blob_and_text_data_types assert_raise ArgumentError do - MysqlAdapter::Column.new("title", "a", Type::Binary.new, "blob") + AbstractMysqlAdapter::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob")) end + text_type = AbstractMysqlAdapter::MysqlTypeMetadata.new( + SqlTypeMetadata.new(type: :text)) assert_raise ArgumentError do - MysqlAdapter::Column.new("title", "Hello", Type::Text.new) + AbstractMysqlAdapter::Column.new("title", "Hello", text_type) end - text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new) + text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type) assert_equal nil, text_column.default - not_null_text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new, "text", false) + not_null_text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type, false) assert_equal "", not_null_text_column.default end def test_has_default_should_return_false_for_blob_and_text_data_types - blob_column = MysqlAdapter::Column.new("title", nil, Type::Binary.new, "blob") + binary_type = SqlTypeMetadata.new(sql_type: "blob") + blob_column = AbstractMysqlAdapter::Column.new("title", nil, binary_type) assert !blob_column.has_default? - text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new) + text_type = SqlTypeMetadata.new(type: :text) + text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type) assert !text_column.has_default? end end - - if current_adapter?(:Mysql2Adapter) - def test_should_set_default_for_mysql_binary_data_types - binary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "binary(1)") - assert_equal "a", binary_column.default - - varbinary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)") - assert_equal "a", varbinary_column.default - end - - def test_should_not_set_default_for_blob_and_text_data_types - assert_raise ArgumentError do - Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "blob") - end - - assert_raise ArgumentError do - Mysql2Adapter::Column.new("title", "Hello", Type::Text.new) - end - - text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new) - assert_equal nil, text_column.default - - not_null_text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new, "text", false) - assert_equal "", not_null_text_column.default - end - - def test_has_default_should_return_false_for_blob_and_text_data_types - blob_column = Mysql2Adapter::Column.new("title", nil, Type::Binary.new, "blob") - assert !blob_column.has_default? - - text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new) - assert !text_column.has_default? - end - end - - if current_adapter?(:PostgreSQLAdapter) - def test_bigint_column_should_map_to_integer - oid = PostgreSQLAdapter::OID::Integer.new - bigint_column = PostgreSQLColumn.new('number', nil, oid, "bigint") - assert_equal :integer, bigint_column.type - end - - def test_smallint_column_should_map_to_integer - oid = PostgreSQLAdapter::OID::Integer.new - smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint") - assert_equal :integer, smallint_column.type - end - end end end end |