diff options
Diffstat (limited to 'activerecord')
86 files changed, 782 insertions, 488 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 0105dfa78c..1470c6dec1 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,37 @@ +* Fix rounding problem for PostgreSQL timestamp column. + + If timestamp column have the precision, it need to format according to + the precision of timestamp column. + + *Ryuta Kamizono* + +* Respect the database default charset for `schema_migrations` table. + + The charset of `version` column in `schema_migrations` table is depend + on the database default charset and collation rather than the encoding + of the connection. + + *Ryuta Kamizono* + +* Raise `ArgumentError` when passing `nil` or `false` to `Relation#merge`. + + These are not valid values to merge in a relation so it should warn the users + early. + + *Rafael Mendonça França* + +* Use `SCHEMA` instead of `DB_STRUCTURE` for specifiying structure file. + + This makes the db:structure tasks consistent with test:load_structure. + + *Dieter Komendera* + +* Respect custom primary keys for associations when calling `Relation#where` + + Fixes #18813. + + *Sean Griffin* + * Fixed several edge cases which could result in a counter cache updating twice or not updating at all for `has_many` and `has_many :through`. diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 1040e6e3bb..39077aea7e 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -245,7 +245,8 @@ module ActiveRecord define_method("#{name}=") do |part| klass = class_name.constantize if part.is_a?(Hash) - part = klass.new(*part.values) + raise ArgumentError unless part.size == part.keys.max + part = klass.new(*part.sort.map(&:last)) end unless part.is_a?(klass) || converter.nil? || part.nil? diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index fdc90df5b6..483d4200bd 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -50,7 +50,12 @@ module ActiveRecord errors = [] callstack.each do |name, values_with_empty_parameters| begin - send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value) + if values_with_empty_parameters.each_value.all?(&:nil?) + values = nil + else + values = values_with_empty_parameters + end + send("#{name}=", values) rescue => ex errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end @@ -82,100 +87,5 @@ module ActiveRecord def find_parameter_position(multiparameter_name) multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i end - - class MultiparameterAttribute #:nodoc: - attr_reader :object, :name, :values, :cast_type - - def initialize(object, name, values) - @object = object - @name = name - @values = values - end - - def read_value - return if values.values.compact.empty? - - @cast_type = object.type_for_attribute(name) - klass = cast_type.klass - - if klass == Time - read_time - elsif klass == Date - read_date - else - read_other - end - end - - private - - def instantiate_time_object(set_values) - if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type) - Time.zone.local(*set_values) - else - Time.send(object.class.default_timezone, *set_values) - end - end - - def read_time - # If column is a :time (and not :date or :datetime) there is no need to validate if - # there are year/month/day fields - if cast_type.type == :time - # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil - { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| - values[key] ||= value - end - else - # else column is a timestamp, so if Date bits were not provided, error - validate_required_parameters!([1,2,3]) - - # If Date bits were provided but blank, then return nil - return if blank_date_parameter? - end - - max_position = extract_max_param(6) - set_values = values.values_at(*(1..max_position)) - # If Time bits are not there, then default to 0 - (3..5).each { |i| set_values[i] = set_values[i].presence || 0 } - instantiate_time_object(set_values) - end - - def read_date - return if blank_date_parameter? - set_values = values.values_at(1,2,3) - begin - Date.new(*set_values) - rescue ArgumentError # if Date.new raises an exception on an invalid date - instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates - end - end - - def read_other - max_position = extract_max_param - positions = (1..max_position) - validate_required_parameters!(positions) - - values.slice(*positions) - end - - # Checks whether some blank date parameter exists. Note that this is different - # than the validate_required_parameters! method, since it just checks for blank - # positions instead of missing ones, and does not raise in case one blank position - # exists. The caller is responsible to handle the case of this returning true. - def blank_date_parameter? - (1..3).any? { |position| values[position].blank? } - end - - # If some position is not provided, it errors out a missing parameter exception. - def validate_required_parameters!(positions) - if missing_parameter = positions.detect { |position| !values.key?(position) } - raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})") - end - end - - def extract_max_param(upper_cap = 100) - [values.keys.max, upper_cap].min - end - end end end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 83b858aae7..553122a5fc 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -15,7 +15,6 @@ module ActiveRecord when false, nil then false else column = self.class.columns_hash[attr_name] - type = self.class.type_for_attribute(attr_name) if column.nil? if Numeric === value || value !~ /[^0-9]/ !value.to_i.zero? @@ -23,7 +22,7 @@ module ActiveRecord return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) !value.blank? end - elsif type.number? + elsif value.respond_to?(:zero?) !value.zero? else !value.blank? diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 24e30b6608..0d989c2eca 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -69,9 +69,18 @@ module ActiveRecord # This method exists to avoid the expensive primary_key check internally, without # breaking compatibility with the read_attribute API - def _read_attribute(attr_name) # :nodoc: - @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? } + if defined?(JRUBY_VERSION) + # This form is significantly faster on JRuby, and this is one of our biggest hotspots. + # https://github.com/jruby/jruby/pull/2562 + def _read_attribute(attr_name, &block) # :nodoc + @attributes.fetch_value(attr_name.to_s, &block) + end + else + def _read_attribute(attr_name) # :nodoc: + @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? } + end end + alias :attribute :_read_attribute private :attribute diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 75cc88d4ee..90c36e4b02 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -2,8 +2,6 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc: - include Type::Decorator - def type_cast_from_database(value) convert_time_to_time_zone(super) end @@ -11,6 +9,8 @@ module ActiveRecord def type_cast_from_user(value) if value.is_a?(Array) value.map { |v| type_cast_from_user(v) } + elsif value.is_a?(Hash) + set_time_zone_without_conversion(super) elsif value.respond_to?(:in_time_zone) begin user_input_in_time_zone(value) || super @@ -20,6 +20,8 @@ module ActiveRecord end end + private + def convert_time_to_time_zone(value) if value.is_a?(Array) value.map { |v| convert_time_to_time_zone(v) } @@ -29,6 +31,10 @@ module ActiveRecord value end end + + def set_time_zone_without_conversion(value) + ::Time.zone.local_to_utc(value).in_time_zone + end end extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 9142317646..013a7d0e01 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -31,8 +31,16 @@ module ActiveRecord attributes.each_key.select { |name| self[name].initialized? } end - def fetch_value(name) - self[name].value { |n| yield n if block_given? } + if defined?(JRUBY_VERSION) + # This form is significantly faster on JRuby, and this is one of our biggest hotspots. + # https://github.com/jruby/jruby/pull/2562 + def fetch_value(name, &block) + self[name].value(&block) + end + else + def fetch_value(name) + self[name].value { |n| yield n if block_given? } + end end def write_from_database(name, value) diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 7cb6b075a0..2c475f3cda 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -1,7 +1,9 @@ module ActiveRecord - module Attributes # :nodoc: + # See ActiveRecord::Attributes::ClassMethods for documentation + module Attributes extend ActiveSupport::Concern + # :nodoc: Type = ActiveRecord::Type included do @@ -9,21 +11,32 @@ module ActiveRecord self.attributes_to_define_after_schema_loads = {} end - module ClassMethods # :nodoc: - # Defines or overrides an attribute on this model. This allows customization of - # Active Record's type casting behavior, as well as adding support for user defined - # types. + module ClassMethods + # Defines an attribute with a type on this model. It will override the + # type of existing attributes if needed. This allows control over how + # values are converted to and from SQL when assigned to a model. It also + # changes the behavior of values passed to + # ActiveRecord::QueryMethods#where. This will let you use + # your domain objects across much of Active Record, without having to + # rely on implementation details or monkey patching. + # + # +name+ The name of the methods to define attribute methods for, and the + # column which this will persist to. + # + # +cast_type+ A symbol such as +:string+ or +:integer+, or a type object + # to be used for this attribute. See the examples below for more + # information about providing custom type objects. # - # +name+ The name of the methods to define attribute methods for, and the column which - # this will persist to. + # ==== Options + # The following options are accepted: # - # +cast_type+ A type object that contains information about how to type cast the value. - # See the examples section for more information. + # +default+ The default value to use when no value is provided. If this option + # is not passed, the previous default value (if any) will be used. + # Otherwise, the default will be +nil+. # - # ==== Options - # The options hash accepts the following options: + # +array+ (PG only) specifies that the type should be an array (see the examples below). # - # +default+ is the default value that the column should use on a new record. + # +range+ (PG only) specifies that the type should be a range (see the examples below). # # ==== Examples # @@ -44,25 +57,64 @@ module ActiveRecord # store_listing.price_in_cents # => BigDecimal.new(10.1) # # class StoreListing < ActiveRecord::Base - # attribute :price_in_cents, Type::Integer.new + # attribute :price_in_cents, :integer # end # # # after # store_listing.price_in_cents # => 10 # - # Users may also define their own custom types, as long as they respond to the methods - # defined on the value type. The +type_cast+ method on your type object will be called - # with values both from the database, and from your controllers. See - # +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your - # type objects inherit from an existing type, or the base value type. + # A default can also be provided. + # + # create_table :store_listings, force: true do |t| + # t.string :my_string, default: "original default" + # end + # + # StoreListing.new.my_string # => "original default" + # + # class StoreListing < ActiveRecord::Base + # attribute :my_string, :string, default: "new default" + # end + # + # StoreListing.new.my_string # => "new default" + # + # Attributes do not need to be backed by a database column. + # + # class MyModel < ActiveRecord::Base + # attribute :my_string, :string + # attribute :my_int_array, :integer, array: true + # attribute :my_float_range, :float, range: true + # end + # + # model = MyModel.new( + # my_string: "string", + # my_int_array: ["1", "2", "3"], + # my_float_range: "[1,3.5]", + # ) + # model.attributes + # # => + # { + # my_string: "string", + # my_int_array: [1, 2, 3], + # my_float_range: 1.0..3.5 + # } + # + # ==== Creating Custom Types + # + # Users may also define their own custom types, as long as they respond + # to the methods defined on the value type. The method + # +type_cast_from_database+ or +type_cast_from_user+ will be called on + # your type object, with raw input from the database or from your + # controllers. See ActiveRecord::Type::Value for the expected API. It is + # recommended that your type objects inherit from an existing type, or + # from ActiveRecord::Type::Value # # class MoneyType < ActiveRecord::Type::Integer - # def type_cast(value) + # def type_cast_from_user(value) # if value.include?('$') # price_in_dollars = value.gsub(/\$/, '').to_f - # price_in_dollars * 100 + # super(price_in_dollars * 100) # else - # value.to_i + # super # end # end # end @@ -73,13 +125,77 @@ module ActiveRecord # # store_listing = StoreListing.new(price_in_cents: '$10.00') # store_listing.price_in_cents # => 1000 + # + # For more details on creating custom types, see the documentation for + # ActiveRecord::Type::Value. + # + # ==== Querying + # + # When ActiveRecord::QueryMethods#where is called, it will + # use the type defined by the model class to convert the value to SQL, + # calling +type_cast_for_database+ on your type object. For example: + # + # class Money < Struct.new(:amount, :currency) + # end + # + # class MoneyType < Type::Value + # def initialize(currency_converter) + # @currency_converter = currency_converter + # end + # + # # value will be the result of +type_cast_from_database+ or + # # +type_cast_from_user+. Assumed to be in instance of +Money+ in + # # this case. + # def type_cast_for_database(value) + # value_in_bitcoins = @currency_converter.convert_to_bitcoins(value) + # value_in_bitcoins.amount + # end + # end + # + # class Product < ActiveRecord::Base + # currency_converter = ConversionRatesFromTheInternet.new + # attribute :price_in_bitcoins, MoneyType.new(currency_converter) + # end + # + # Product.where(price_in_bitcoins: Money.new(5, "USD")) + # # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230 + # + # Product.where(price_in_bitcoins: Money.new(5, "GBP")) + # # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412 + # + # ==== Dirty Tracking + # + # The type of an attribute is given the opportunity to change how dirty + # tracking is performed. The methods +changed?+ and +changed_in_place?+ + # will be called from ActiveModel::Dirty. See the documentation for those + # methods in ActiveRecord::Type::Value for more details. def attribute(name, cast_type, **options) name = name.to_s reload_schema_from_cache - self.attributes_to_define_after_schema_loads = attributes_to_define_after_schema_loads.merge(name => [cast_type, options]) + self.attributes_to_define_after_schema_loads = + attributes_to_define_after_schema_loads.merge( + name => [cast_type, options] + ) end + # This is the low level API which sits beneath +attribute+. It only + # accepts type objects, and will do its work immediately instead of + # waiting for the schema to load. Automatic schema detection and + # ClassMethods#attribute both call this under the hood. While this method + # is provided so it can be used by plugin authors, application code + # should probably use ClassMethods#attribute. + # + # +name+ The name of the attribute being defined. Expected to be a +String+. + # + # +cast_type+ The type object to use for this attribute. + # + # +default+ The default value to use when no value is provided. If this option + # is not passed, the previous default value (if any) will be used. + # Otherwise, the default will be +nil+. + # + # +user_provided_default+ Whether the default value should be cast using + # +type_cast_from_user+ or +type_cast_from_database+. def define_attribute( name, cast_type, @@ -90,10 +206,14 @@ module ActiveRecord define_default_attribute(name, default, cast_type, from_user: user_provided_default) end - def load_schema! + def load_schema! # :nodoc: super attributes_to_define_after_schema_loads.each do |name, (type, options)| - define_attribute(name, type, **options) + if type.is_a?(Symbol) + type = connection.type_for_attribute_options(type, **options.except(:default)) + end + + define_attribute(name, type, **options.slice(:default)) end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 2c013a074a..55d3360070 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -134,8 +134,29 @@ module ActiveRecord binds.map(&:value_for_database) end + def type_for_attribute_options(type_name, **options) + klass = type_classes_with_standard_constructor.fetch(type_name, Type::Value) + klass.new(**options) + end + private + def type_classes_with_standard_constructor + { + big_integer: Type::BigInteger, + binary: Type::Binary, + boolean: Type::Boolean, + date: Type::Date, + date_time: Type::DateTime, + decimal: Type::Decimal, + float: Type::Float, + integer: Type::Integer, + string: Type::String, + text: Type::Text, + time: Type::Time, + } + end + def types_which_need_no_typecasting [nil, Numeric, String] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index db20b60d60..81f8615976 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -28,7 +28,7 @@ module ActiveRecord end def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) + o.sql_type ||= type_to_sql(o.type, o.limit, o.precision, o.scale) column_sql = "#{quote_column_name(o.name)} #{o.sql_type}" add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 1eb30956d4..11440e30d4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -185,7 +185,7 @@ module ActiveRecord ensure unless error if Thread.current.status == 'aborting' - rollback_transaction + rollback_transaction if transaction else begin commit_transaction diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 67c8f438e2..9392bcb473 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -367,8 +367,17 @@ module ActiveRecord end def case_insensitive_comparison(table, attribute, column, value) - table[attribute].lower.eq(table.lower(value)) + if can_perform_case_insensitive_comparison_for?(column) + table[attribute].lower.eq(table.lower(value)) + else + case_sensitive_comparison(table, attribute, column, value) + end + end + + def can_perform_case_insensitive_comparison_for?(column) + true end + private :can_perform_case_insensitive_comparison_for? def current_savepoint_name current_transaction.savepoint_name 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 5c8c4b883a..c29692d6ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -63,7 +63,7 @@ module ActiveRecord def column_spec_for_primary_key(column) spec = {} - if column.extra == 'auto_increment' + if column.auto_increment? return unless column.limit == 8 spec[:id] = ':bigint' else @@ -103,6 +103,10 @@ module ActiveRecord collation && !collation.match(/_ci$/) end + def auto_increment? + extra == 'auto_increment' + end + private # MySQL misreports NOT NULL column default when none is given. @@ -808,7 +812,7 @@ module ActiveRecord options = { default: column.default, null: column.null, - auto_increment: column.extra == "auto_increment" + auto_increment: column.auto_increment? } current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] @@ -913,18 +917,10 @@ module ActiveRecord end class MysqlDateTime < Type::DateTime # :nodoc: - def type_cast_for_database(value) - if value.acts_like?(:time) && value.respond_to?(:usec) - result = super.to_s(:db) - case precision - when 1..6 - "#{result}.#{sprintf("%0#{precision}d", value.usec / 10 ** (6 - precision))}" - else - result - end - else - super - end + private + + def has_precision? + precision || 0 end end @@ -947,6 +943,10 @@ module ActiveRecord end end end + + def type_classes_with_standard_constructor + super.merge(string: MysqlString, date_time: MysqlDateTime) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 75f244b3f3..fac6f81540 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -39,7 +39,7 @@ module ActiveRecord MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191 def initialize_schema_migrations_table - if @config[:encoding] == 'utf8mb4' + if charset == 'utf8mb4' ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4) else ActiveRecord::SchemaMigration.create_table diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index e45a2f59d9..2608a2abab 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Array < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable # Loads pg_array_parser if available. String parsing can be # performed quicker by a native extension, which will not create @@ -48,6 +48,12 @@ module ActiveRecord end end + def ==(other) + other.is_a?(Array) && + subtype == other.subtype && + delimiter == other.delimiter + end + private def type_cast_array(value, method) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb index b9e7894e5c..2fe61eeb77 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -5,6 +5,15 @@ module ActiveRecord class DateTime < Type::DateTime # :nodoc: include Infinity + def type_cast_for_database(value) + if has_precision? && value.acts_like?(:time) && value.year <= 0 + bce_year = format("%04d", -value.year + 1) + super.sub(/^-?\d+/, bce_year) + " BC" + else + super + end + end + def cast_value(value) if value.is_a?(::String) case value diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index be4525c94f..b46e50c865 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Hstore < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable def type :hstore diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index 7dadc09a44..13dd037314 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Json < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable def type :json diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index bac8b01d6b..4084725ed7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Point < Type::Value # :nodoc: - include Type::Mutable + include Type::Helpers::Mutable def type :point diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index 3adfb8b9d8..2a5a59fbc6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -7,7 +7,7 @@ module ActiveRecord class Range < Type::Value # :nodoc: attr_reader :subtype, :type - def initialize(subtype, type) + def initialize(subtype, type = :range) @subtype = subtype @type = type end @@ -40,6 +40,12 @@ module ActiveRecord end end + def ==(other) + other.is_a?(Range) && + other.subtype == subtype && + other.type == type + end + private def type_cast_single(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index b2a42e9ebb..2d2fede4e8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -8,10 +8,6 @@ module ActiveRecord def initialize(type) @type = type end - - def text? - false - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 464adb4e23..11114f32fe 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -65,6 +65,23 @@ module ActiveRecord type_map.lookup(column.oid, column.fmod, column.sql_type) end + def type_for_attribute_options( + type_name, + array: false, + range: false, + **options + ) + if array + subtype = type_for_attribute_options(type_name, **options) + OID::Array.new(subtype) + elsif range + subtype = type_for_attribute_options(type_name, **options) + OID::Range.new(subtype) + else + super(type_name, **options) + end + end + private def _quote(value) @@ -103,6 +120,30 @@ module ActiveRecord super end end + + def type_classes_with_standard_constructor + super.merge( + bit: OID::Bit, + bit_varying: OID::BitVarying, + binary: OID::Bytea, + cidr: OID::Cidr, + date: OID::Date, + date_time: OID::DateTime, + decimal: OID::Decimal, + enum: OID::Enum, + float: OID::Float, + hstore: OID::Hstore, + inet: OID::Inet, + json: OID::Json, + jsonb: OID::Jsonb, + money: OID::Money, + point: OID::Point, + time: OID::Time, + uuid: OID::Uuid, + vector: OID::Vector, + xml: OID::Xml, + ) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index a9522e152f..b9078d4c86 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -76,15 +76,15 @@ module ActiveRecord column(name, :point, options) end - def bit(name, options) + def bit(name, options = {}) column(name, :bit, options) end - def bit_varying(name, options) + def bit_varying(name, options = {}) column(name, :bit_varying, options) end - def money(name, options) + def money(name, options = {}) column(name, :money, options) end 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 d4e183dd16..750eaeca92 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -4,16 +4,9 @@ module ActiveRecord class SchemaCreation < AbstractAdapter::SchemaCreation private - def column_options(o) - column_options = super - column_options[:array] = o.array - column_options - end - - def add_column_options!(sql, options) - if options[:array] - sql << '[]' - end + def visit_ColumnDefinition(o) + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) + o.sql_type << '[]' if o.array super end @@ -24,14 +17,6 @@ module ActiveRecord super end end - - def type_for_column(column) - if column.array - @conn.lookup_cast_type("#{column.sql_type}[]") - else - super - end - end end module SchemaStatements diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index ab970e183a..e8ecaffcab 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -762,6 +762,24 @@ module ActiveRecord def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as end + + def can_perform_case_insensitive_comparison_for?(column) + @case_insensitive_cache ||= {} + @case_insensitive_cache[column.sql_type] ||= begin + sql = <<-end_sql + SELECT exists( + SELECT * FROM pg_proc + INNER JOIN pg_cast + ON casttarget::text::oidvector = proargtypes + WHERE proname = 'lower' + AND castsource = '#{column.sql_type}'::regtype::oid + ) + end_sql + execute_and_clear(sql, "SCHEMA", []) do |result| + result.getvalue(0, 0) == 't' + end + end + 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 c06213a7bf..edd060248f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -240,6 +240,10 @@ module ActiveRecord end end + def type_classes_with_standard_constructor + super.merge(binary: SQLite3Binary) + end + def quote_string(s) #:nodoc: @connection.class.quote(s) end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index c1eaa4f49b..44d587206d 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -107,20 +107,18 @@ module ActiveRecord super end - def initialize_find_by_cache + def initialize_find_by_cache # :nodoc: self.find_by_statement_cache = {}.extend(Mutex_m) end - def inherited(child_class) + def inherited(child_class) # :nodoc: child_class.initialize_find_by_cache super end - def find(*ids) + def find(*ids) # :nodoc: # We don't have cache keys for this stuff yet return super unless ids.length == 1 - # Allow symbols to super to maintain compatibility for deprecated finders until Rails 5 - return super if ids.first.kind_of?(Symbol) return super if block_given? || primary_key.nil? || default_scopes.any? || @@ -152,7 +150,7 @@ module ActiveRecord raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'" end - def find_by(*args) + def find_by(*args) # :nodoc: return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any? return super if default_scopes.any? @@ -185,11 +183,11 @@ module ActiveRecord end end - def find_by!(*args) + def find_by!(*args) # :nodoc: find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}") end - def initialize_generated_modules + def initialize_generated_modules # :nodoc: generated_association_methods end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 008cda46cd..c5b10fcddf 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -184,7 +184,7 @@ module ActiveRecord end end - class LockingType < SimpleDelegator # :nodoc: + class LockingType < DelegateClass(Type::Value) # :nodoc: def type_cast_from_database(value) # `nil` *should* be changed to 0 super.to_i diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 04c2be045d..dde4dfa83c 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -269,9 +269,9 @@ db_namespace = namespace :db do end namespace :structure do - desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' + desc 'Dump the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' task :dump => [:environment, :load_config] do - filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") + filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) @@ -287,7 +287,7 @@ db_namespace = namespace :db do desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['DB_STRUCTURE']) + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA']) end task :load_if_sql => ['db:create', :environment] do diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 4f0502ae75..9d690af11d 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -45,19 +45,19 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_each(options = {}) + def find_each(start: nil, batch_size: 1000) if block_given? - find_in_batches(options) do |records| + find_in_batches(start: start, batch_size: batch_size) do |records| records.each { |record| yield record } end else - enum_for :find_each, options do - options[:start] ? where(table[primary_key].gteq(options[:start])).size : size + enum_for(:find_each, start: start, batch_size: batch_size) do + start ? where(table[primary_key].gteq(start)).size : size end end end - # Yields each batch of records that was found by the find +options+ as + # Yields each batch of records that was found by the find options as # an array. # # Person.where("age > 21").find_in_batches do |group| @@ -95,15 +95,11 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_in_batches(options = {}) - options.assert_valid_keys(:start, :batch_size) - + def find_in_batches(start: nil, batch_size: 1000) relation = self - start = options[:start] - batch_size = options[:batch_size] || 1000 unless block_given? - return to_enum(:find_in_batches, options) do + return to_enum(:find_in_batches, start: start, batch_size: batch_size) do total = start ? where(table[primary_key].gteq(start)).size : size (total - 1).div(batch_size) + 1 end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb index aabcf20c1d..159889d3b8 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -31,7 +31,14 @@ module ActiveRecord end def ids - value + case value + when Relation + value.select(primary_key) + when Array + value.map { |v| convert_to_id(v) } + else + convert_to_id(value) + end end def base_class @@ -42,6 +49,10 @@ module ActiveRecord private + def primary_key + associated_table.association_primary_key(base_class) + end + def polymorphic_base_class_from_value case value when Relation @@ -53,6 +64,15 @@ module ActiveRecord value.class.base_class end end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key) + else + value + end + end end end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index dd3610d7aa..70da37fa84 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -32,7 +32,7 @@ module ActiveRecord elsif other spawn.merge!(other) else - self + raise ArgumentError, "invalid argument: #{other.inspect}." end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 35420e6551..a083deafe1 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -30,7 +30,13 @@ module ActiveRecord end def default_scoped # :nodoc: - relation.merge(build_default_scope) + scope = build_default_scope + + if scope + relation.spawn.merge!(scope) + else + relation + end end # Collects attributes from scopes that should be applied when creating diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 6c8792ee80..3dd6321a97 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -1,6 +1,7 @@ module ActiveRecord class TableMetadata # :nodoc: delegate :foreign_type, :foreign_key, to: :association, prefix: true + delegate :association_primary_key, to: :association def initialize(klass, arel_table, association = nil) @klass = klass diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 250e8d5b23..f18b076d58 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,7 +1,4 @@ -require 'active_record/type/decorator' -require 'active_record/type/mutable' -require 'active_record/type/numeric' -require 'active_record/type/time_value' +require 'active_record/type/helpers' require 'active_record/type/value' require 'active_record/type/big_integer' diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb index d90a6069b7..3ceab59ebb 100644 --- a/activerecord/lib/active_record/type/date.rb +++ b/activerecord/lib/active_record/type/date.rb @@ -1,14 +1,12 @@ module ActiveRecord module Type class Date < Value # :nodoc: + include Helpers::AcceptsMultiparameterTime.new + def type :date end - def klass - ::Date - end - def type_cast_for_schema(value) "'#{value.to_s(:db)}'" end @@ -41,6 +39,11 @@ module ActiveRecord ::Date.new(year, mon, mday) rescue nil end end + + def value_from_multiparameter_assignment(*) + time = super + time && time.to_date + end end end end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index 0a737815bc..e8614b16e0 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -1,28 +1,38 @@ module ActiveRecord module Type class DateTime < Value # :nodoc: - include TimeValue + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 4 => 0, 5 => 0 } + ) def type :datetime end def type_cast_for_database(value) + return super unless value.acts_like?(:time) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal - if value.acts_like?(:time) - if value.respond_to?(zone_conversion_method) - value.send(zone_conversion_method) - else - value - end + if value.respond_to?(zone_conversion_method) + value = value.send(zone_conversion_method) + end + + return value unless has_precision? + + result = value.to_s(:db) + if value.respond_to?(:usec) && (1..6).cover?(precision) + "#{result}.#{sprintf("%0#{precision}d", value.usec / 10 ** (6 - precision))}" else - super + result end end private + alias has_precision? precision + def cast_value(string) return string unless string.is_a?(::String) return if string.empty? @@ -42,6 +52,14 @@ module ActiveRecord new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) end + + def value_from_multiparameter_assignment(values_hash) + missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } + if missing_parameter + raise ArgumentError, missing_parameter + end + super + end end end end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index 7b2bee2c42..867b5f75c7 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type class Decimal < Value # :nodoc: - include Numeric + include Helpers::Numeric def type :decimal diff --git a/activerecord/lib/active_record/type/decorator.rb b/activerecord/lib/active_record/type/decorator.rb deleted file mode 100644 index 9fce38ea44..0000000000 --- a/activerecord/lib/active_record/type/decorator.rb +++ /dev/null @@ -1,14 +0,0 @@ -module ActiveRecord - module Type - module Decorator # :nodoc: - def init_with(coder) - @subtype = coder['subtype'] - __setobj__(@subtype) - end - - def encode_with(coder) - coder['subtype'] = __getobj__ - end - end - end -end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb index 42eb44b9a9..0a9088e0a1 100644 --- a/activerecord/lib/active_record/type/float.rb +++ b/activerecord/lib/active_record/type/float.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type class Float < Value # :nodoc: - include Numeric + include Helpers::Numeric def type :float diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb new file mode 100644 index 0000000000..634d417d13 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers.rb @@ -0,0 +1,4 @@ +require 'active_record/type/helpers/accepts_multiparameter_time' +require 'active_record/type/helpers/numeric' +require 'active_record/type/helpers/mutable' +require 'active_record/type/helpers/time_value' diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb new file mode 100644 index 0000000000..640943c5e9 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb @@ -0,0 +1,30 @@ +module ActiveRecord + module Type + module Helpers + class AcceptsMultiparameterTime < Module # :nodoc: + def initialize(defaults: {}) + define_method(:type_cast_from_user) do |value| + if value.is_a?(Hash) + value_from_multiparameter_assignment(value) + else + super(value) + end + end + + define_method(:value_from_multiparameter_assignment) do |values_hash| + defaults.each do |k, v| + values_hash[k] ||= v + end + return unless values_hash[1] && values_hash[2] && values_hash[3] + values = values_hash.sort.map(&:last) + ::Time.send( + ActiveRecord::Base.default_timezone, + *values + ) + end + private :value_from_multiparameter_assignment + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activerecord/lib/active_record/type/helpers/mutable.rb new file mode 100644 index 0000000000..dc37f4a885 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/mutable.rb @@ -0,0 +1,18 @@ +module ActiveRecord + module Type + module Helpers + module Mutable # :nodoc: + def type_cast_from_user(value) + type_cast_from_database(type_cast_for_database(value)) + end + + # +raw_old_value+ will be the `_before_type_cast` version of the + # value (likely a string). +new_value+ will be the current, type + # cast value. + def changed_in_place?(raw_old_value, new_value) + raw_old_value != type_cast_for_database(new_value) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activerecord/lib/active_record/type/helpers/numeric.rb new file mode 100644 index 0000000000..b0d4f03117 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/numeric.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module Type + module Helpers + module Numeric # :nodoc: + def type_cast(value) + value = case value + when true then 1 + when false then 0 + when ::String then value.presence + else value + end + super(value) + end + + def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: + super || number_to_non_number?(old_value, new_value_before_type_cast) + end + + private + + def number_to_non_number?(old_value, new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast) + end + + def non_numeric_string?(value) + # 'wibble'.to_i will give zero, we want to make sure + # that we aren't marking int zero to string zero as + # changed. + value.to_s !~ /\A-?\d+\.?\d*\z/ + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activerecord/lib/active_record/type/helpers/time_value.rb new file mode 100644 index 0000000000..6e14c3a9b5 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/time_value.rb @@ -0,0 +1,40 @@ +module ActiveRecord + module Type + module Helpers + module TimeValue # :nodoc: + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + def user_input_in_time_zone(value) + value.in_time_zone + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return unless time + + time -= offset + Base.default_timezone == :utc ? time : time.getlocal + else + ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb index 90ca9f88da..2ab2402dfd 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activerecord/lib/active_record/type/integer.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type class Integer < Value # :nodoc: - include Numeric + include Helpers::Numeric # Column storage size in bytes. # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc. diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb deleted file mode 100644 index 066617ea59..0000000000 --- a/activerecord/lib/active_record/type/mutable.rb +++ /dev/null @@ -1,16 +0,0 @@ -module ActiveRecord - module Type - module Mutable # :nodoc: - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) - end - - # +raw_old_value+ will be the `_before_type_cast` version of the - # value (likely a string). +new_value+ will be the current, type - # cast value. - def changed_in_place?(raw_old_value, new_value) - raw_old_value != type_cast_for_database(new_value) - end - end - end -end diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb deleted file mode 100644 index 674f996f38..0000000000 --- a/activerecord/lib/active_record/type/numeric.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveRecord - module Type - module Numeric # :nodoc: - def number? - true - end - - def type_cast(value) - value = case value - when true then 1 - when false then 0 - when ::String then value.presence - else value - end - super(value) - end - - def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: - super || number_to_non_number?(old_value, new_value_before_type_cast) - end - - private - - def number_to_non_number?(old_value, new_value_before_type_cast) - old_value != nil && non_numeric_string?(new_value_before_type_cast) - end - - def non_numeric_string?(value) - # 'wibble'.to_i will give zero, we want to make sure - # that we aren't marking int zero to string zero as - # changed. - value.to_s !~ /\A-?\d+\.?\d*\z/ - end - end - end -end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 3cac03464e..6c6c520048 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,8 +1,7 @@ module ActiveRecord module Type class Serialized < DelegateClass(Type::Value) # :nodoc: - include Mutable - include Decorator + include Helpers::Mutable attr_reader :subtype, :coder @@ -36,16 +35,6 @@ module ActiveRecord ActiveRecord::Store::IndifferentHashAccessor end - def init_with(coder) - @coder = coder['coder'] - super - end - - def encode_with(coder) - coder['coder'] = @coder - super - end - private def default_value?(value) diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index cf95e25be0..fbc0af2c5a 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -21,10 +21,6 @@ module ActiveRecord end end - def text? - true - end - private def cast_value(value) diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb index cab1c7bf1e..19a10021bc 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -1,7 +1,10 @@ module ActiveRecord module Type class Time < Value # :nodoc: - include TimeValue + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } + ) def type :time diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb deleted file mode 100644 index 8d9ac25643..0000000000 --- a/activerecord/lib/active_record/type/time_value.rb +++ /dev/null @@ -1,42 +0,0 @@ -module ActiveRecord - module Type - module TimeValue # :nodoc: - def klass - ::Time - end - - def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" - end - - def user_input_in_time_zone(value) - value.in_time_zone - end - - private - - def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) - # Treat 0000-00-00 00:00:00 as nil. - return if year.nil? || (year == 0 && mon == 0 && mday == 0) - - if offset - time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil - return unless time - - time -= offset - Base.default_timezone == :utc ? time : time.getlocal - else - ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 859b51ca90..7338920f3b 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -1,34 +1,37 @@ module ActiveRecord module Type - class Value # :nodoc: + class Value attr_reader :precision, :scale, :limit - # Valid options are +precision+, +scale+, and +limit+. - def initialize(options = {}) - options.assert_valid_keys(:precision, :scale, :limit) - @precision = options[:precision] - @scale = options[:scale] - @limit = options[:limit] + def initialize(precision: nil, limit: nil, scale: nil) + @precision = precision + @scale = scale + @limit = limit end - # The simplified type that this object represents. Returns a symbol such - # as +:string+ or +:integer+ - def type; end + def type # :nodoc: + end - # Type casts a string from the database into the appropriate ruby type. - # Classes which do not need separate type casting behavior for database - # and user provided values should override +cast_value+ instead. + # Convert a value from database input to the appropriate ruby type. The + # return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. See also + # Value#type_cast and Value#cast_value. + # + # +value+ The raw input, as provided from the database. def type_cast_from_database(value) type_cast(value) end # Type casts a value from user input (e.g. from a setter). This value may - # be a string from the form builder, or an already type cast value - # provided manually to a setter. + # be a string from the form builder, or a ruby object passed to a setter. + # There is currently no way to differentiate between which source it came + # from. # - # Classes which do not need separate type casting behavior for database - # and user provided values should override +type_cast+ or +cast_value+ - # instead. + # The return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. See also: + # Value#type_cast and Value#cast_value. + # + # +value+ The raw input, as provided to the attribute setter. def type_cast_from_user(value) type_cast(value) end @@ -36,7 +39,7 @@ module ActiveRecord # Cast a value from the ruby type to a type that the database knows how # to understand. The returned value from this method should be a # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or - # +nil+ + # +nil+. def type_cast_for_database(value) value end @@ -49,21 +52,10 @@ module ActiveRecord # These predicates are not documented, as I need to look further into # their use, and see if they can be removed entirely. - def text? # :nodoc: - false - end - - def number? # :nodoc: - false - end - def binary? # :nodoc: false end - def klass # :nodoc: - end - # Determines whether a value has changed for dirty checking. +old_value+ # and +new_value+ will always be type-cast. Types should not need to # override this method. @@ -72,10 +64,23 @@ module ActiveRecord end # Determines whether the mutable value has been modified since it was - # read. Returns +false+ by default. This method should not be overridden - # directly. Types which return a mutable value should include - # +Type::Mutable+, which will define this method. - def changed_in_place?(*) + # read. Returns +false+ by default. If your type returns an object + # which could be mutated, you should override this method. You will need + # to either: + # + # - pass +new_value+ to Value#type_cast_for_database and compare it to + # +raw_old_value+ + # + # or + # + # - pass +raw_old_value+ to Value#type_cast_from_database and compare it to + # +new_value+ + # + # +raw_old_value+ The original value, before being passed to + # +type_cast_from_database+. + # + # +new_value+ The current value, after type casting. + def changed_in_place?(raw_old_value, new_value) false end @@ -88,14 +93,17 @@ module ActiveRecord private - def type_cast(value) + # Convenience method. If you don't need separate behavior for + # Value#type_cast_from_database and Value#type_cast_from_user, you can override + # this method instead. The default behavior of both methods is to call + # this one. See also Value#cast_value. + def type_cast(value) # :doc: cast_value(value) unless value.nil? end # Convenience method for types which do not need separate type casting - # behavior for user and database inputs. Called by - # +type_cast_from_database+ and +type_cast_from_user+ for all values - # except +nil+. + # behavior for user and database inputs. Called by Value#type_cast for + # values except +nil+. def cast_value(value) # :doc: value end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index ad56f637e3..a766d77e88 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -69,7 +69,7 @@ module ActiveRecord value = Arel::Nodes::Quoted.new(value) - comparison = if !options[:case_sensitive] && value && cast_type.text? + comparison = if !options[:case_sensitive] && !value.nil? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 06b0cb5515..271b570eb5 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -18,21 +18,29 @@ module ActiveRecord smtn = ActiveRecord::Migrator.schema_migrations_table_name connection.drop_table smtn, if_exists: true - config = connection.instance_variable_get(:@config) - original_encoding = config[:encoding] + database_name = connection.current_database + database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'") + + original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"] + original_collation = database_info["DEFAULT_COLLATION_NAME"] + + execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4") - config[:encoding] = 'utf8mb4' connection.initialize_schema_migrations_table assert connection.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4) ensure - config[:encoding] = original_encoding + execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}") end private def connection @connection ||= ActiveRecord::Base.connection end + + def execute(sql) + connection.execute(sql) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 35b792b1c2..3d5b7e5137 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -36,14 +36,11 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal :string, @column.type assert_equal "character varying", @column.sql_type assert @column.array? - assert_not @type.number? assert_not @type.binary? ratings_column = PgArray.columns_hash['ratings'] assert_equal :integer, ratings_column.type - type = PgArray.type_for_attribute("ratings") assert ratings_column.array? - assert_not type.number? end def test_default diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index d6b976a6d0..6c6b4dc22a 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -13,6 +13,8 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase @connection.create_table('postgresql_bit_strings', :force => true) do |t| t.bit :a_bit, default: "00000011", limit: 8 t.bit_varying :a_bit_varying, default: "0011", limit: 4 + t.bit :another_bit + t.bit_varying :another_bit_varying end end @@ -28,7 +30,6 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase assert_not column.array? type = PostgresqlBitString.type_for_attribute("a_bit") - assert_not type.number? assert_not type.binary? end @@ -39,7 +40,6 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase assert_not column.array? type = PostgresqlBitString.type_for_attribute("a_bit_varying") - assert_not type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index 077f0271e2..0ee2a21484 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -34,7 +34,6 @@ if ActiveRecord::Base.connection.supports_extensions? assert_not column.array? type = Citext.type_for_attribute('cival') - assert_not type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 0c0d2465b2..83dfb18e95 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -52,7 +52,6 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase assert_not column.array? type = PostgresqlComposite.type_for_attribute("address") - assert_not type.number? assert_not type.binary? end @@ -115,7 +114,6 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase assert_not column.array? type = PostgresqlComposite.type_for_attribute("address") - assert_not type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index 702de07597..b7d776b40c 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -31,7 +31,6 @@ class PostgresqlDomainTest < ActiveRecord::TestCase assert_not column.array? type = PostgresqlDomain.type_for_attribute("price") - assert type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 7739d2ee3b..acb09b0607 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -33,7 +33,6 @@ class PostgresqlEnumTest < ActiveRecord::TestCase assert_not column.array? type = PostgresqlEnum.type_for_attribute("current_mood") - assert_not type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index 8357dcb3dc..81891a90fa 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -23,7 +23,6 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase assert_not column.array? type = Tsvector.type_for_attribute("text_vector") - assert_not type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 5d2e86c5a0..4b25381a83 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -28,7 +28,6 @@ class PostgresqlPointTest < ActiveRecord::TestCase assert_not column.array? type = PostgresqlPoint.type_for_attribute("x") - assert_not type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 121f347986..11053a6e38 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -56,7 +56,6 @@ if ActiveRecord::Base.connection.supports_extensions? assert_equal "hstore", @column.sql_type assert_not @column.array? - assert_not @type.number? assert_not @type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index dd7b67bad7..cbe7e62870 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -36,7 +36,6 @@ module PostgresqlJSONSharedTestCases assert_not column.array? type = JsonDataType.type_for_attribute("payload") - assert_not type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index ca17edfd03..2b3823f9f1 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -32,7 +32,6 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase assert_not column.array? type = Ltree.type_for_attribute('path') - assert_not type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 4288f754c0..ba9af4be6f 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -10,8 +10,8 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection @connection.execute("set lc_monetary = 'C'") @connection.create_table('postgresql_moneys', force: true) do |t| - t.column "wealth", "money" - t.column "depth", "money", default: "150.55" + t.money "wealth" + t.money "depth", default: "150.55" end end @@ -27,7 +27,6 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase assert_not column.array? type = PostgresqlMoney.type_for_attribute("wealth") - assert type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index efe754ac7c..4cd2d4d5f3 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -25,7 +25,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_not column.array? type = PostgresqlNetworkAddress.type_for_attribute("cidr_address") - assert_not type.number? assert_not type.binary? end @@ -36,7 +35,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_not column.array? type = PostgresqlNetworkAddress.type_for_attribute("inet_address") - assert_not type.number? assert_not type.binary? end @@ -47,7 +45,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_not column.array? type = PostgresqlNetworkAddress.type_for_attribute("mac_address") - assert_not type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index eb32c4d2c2..8246b14b93 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -46,6 +46,8 @@ end class TimestampTest < ActiveRecord::TestCase fixtures :topics + class Foo < ActiveRecord::Base; end + def test_group_by_date keys = Topic.group("date_trunc('month', created_at)").count.keys assert_operator keys.length, :>, 0 @@ -135,6 +137,20 @@ class TimestampTest < ActiveRecord::TestCase assert_equal date, Developer.find_by_name("yahagi").updated_at end + def test_formatting_timestamp_according_to_precision + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.datetime :created_at, precision: 0 + t.datetime :updated_at, precision: 4 + end + date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999) + Foo.create!(created_at: date, updated_at: date) + assert foo = Foo.find_by(created_at: date) + assert_equal date.to_s, foo.created_at.to_s + assert_equal date.to_s, foo.updated_at.to_s + assert_equal 000000, foo.created_at.usec + assert_equal 999900, foo.updated_at.usec + end + private def pg_datetime_precision(table_name, column_name) diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 1d0f013a26..6693843497 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -51,7 +51,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_not column.array? type = UUIDType.type_for_attribute("guid") - assert_not type.number? assert_not type.binary? end diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb index 9ad02ffae8..0b96319cbd 100644 --- a/activerecord/test/cases/attribute_decorators_test.rb +++ b/activerecord/test/cases/attribute_decorators_test.rb @@ -51,7 +51,7 @@ module ActiveRecord end test "undecorated columns are not touched" do - Model.attribute :another_string, Type::String.new, default: 'something or other' + Model.attribute :another_string, :string, default: 'something or other' Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } assert_equal 'something or other', Model.new.another_string @@ -86,7 +86,7 @@ module ActiveRecord end test "decorating attributes does not modify parent classes" do - Model.attribute :another_string, Type::String.new, default: 'whatever' + Model.attribute :another_string, :string, default: 'whatever' Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } child_class = Class.new(Model) child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) } @@ -110,7 +110,7 @@ module ActiveRecord end test "decorating with a proc" do - Model.attribute :an_int, Type::Integer.new + Model.attribute :an_int, :integer type_is_integer = proc { |_, type| type.type == :integer } Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type| Multiplier.new(type) diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 6f331c5985..a753e8b74e 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -1,17 +1,17 @@ require 'cases/helper' class OverloadedType < ActiveRecord::Base - attribute :overloaded_float, Type::Integer.new - attribute :overloaded_string_with_limit, Type::String.new(limit: 50) - attribute :non_existent_decimal, Type::Decimal.new - attribute :string_with_default, Type::String.new, default: 'the overloaded default' + attribute :overloaded_float, :integer + attribute :overloaded_string_with_limit, :string, limit: 50 + attribute :non_existent_decimal, :decimal + attribute :string_with_default, :string, default: 'the overloaded default' end class ChildOfOverloadedType < OverloadedType end class GrandchildOfOverloadedType < ChildOfOverloadedType - attribute :overloaded_float, Type::Float.new + attribute :overloaded_float, :float end class UnoverloadedType < ActiveRecord::Base @@ -124,5 +124,37 @@ module ActiveRecord assert_equal "from user", model.wibble end + + if current_adapter?(:PostgreSQLAdapter) + test "arrays types can be specified" do + klass = Class.new(OverloadedType) do + attribute :my_array, :string, limit: 50, array: true + attribute :my_int_array, :integer, array: true + end + + string_array = ConnectionAdapters::PostgreSQL::OID::Array.new( + Type::String.new(limit: 50)) + int_array = ConnectionAdapters::PostgreSQL::OID::Array.new( + Type::Integer.new) + refute_equal string_array, int_array + assert_equal string_array, klass.type_for_attribute("my_array") + assert_equal int_array, klass.type_for_attribute("my_int_array") + end + + test "range types can be specified" do + klass = Class.new(OverloadedType) do + attribute :my_range, :string, limit: 50, range: true + attribute :my_int_range, :integer, range: true + end + + string_range = ConnectionAdapters::PostgreSQL::OID::Range.new( + Type::String.new(limit: 50)) + int_range = ConnectionAdapters::PostgreSQL::OID::Range.new( + Type::Integer.new) + refute_equal string_range, int_range + assert_equal string_range, klass.type_for_attribute("my_range") + assert_equal int_range, klass.type_for_attribute("my_int_range") + end + end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 7c5939fc47..ef1173a2ba 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -903,8 +903,8 @@ class BasicsTest < ActiveRecord::TestCase class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' - attribute :my_house_population, Type::Integer.new - attribute :atoms_in_universe, Type::Integer.new + attribute :my_house_population, :integer + attribute :atoms_in_universe, :integer end def test_big_decimal_conditions diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index f47568f2f5..f0393aa6b1 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -15,9 +15,9 @@ require 'models/treasure' class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' - attribute :world_population, Type::Integer.new - attribute :my_house_population, Type::Integer.new - attribute :atoms_in_universe, Type::Integer.new + attribute :world_population, :integer + attribute :my_house_population, :integer + attribute :atoms_in_universe, :integer end class CalculationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 192ba6f7cd..c2573ac72b 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -711,7 +711,7 @@ class DirtyTest < ActiveRecord::TestCase test "attribute_will_change! doesn't try to save non-persistable attributes" do klass = Class.new(ActiveRecord::Base) do self.table_name = 'people' - attribute :non_persisted_attribute, ActiveRecord::Type::String.new + attribute :non_persisted_attribute, :string end record = klass.new(first_name: "Sean") diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 54d3a729c3..6f65288ac0 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -65,7 +65,7 @@ module ActiveRecord if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_mysql_rename_column_preserves_auto_increment rename_column "test_models", "id", "id_test" - assert_equal "auto_increment", connection.columns("test_models").find { |c| c.name == "id_test" }.extra + assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment? TestModel.reset_column_information ensure rename_column "test_models", "id_test", "id" diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 333fb7d4c6..66e2175c48 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'active_support/testing/stream' require 'support/ddl_helper' require 'support/schema_dumping_helper' @@ -8,6 +9,7 @@ module ActiveRecord class ForeignKeyTest < ActiveRecord::TestCase include DdlHelper include SchemaDumpingHelper + include ActiveSupport::Testing::Stream class Rocket < ActiveRecord::Base end @@ -221,17 +223,6 @@ module ActiveRecord silence_stream($stdout) { migration.migrate(:down) } end - private - - def silence_stream(stream) - old_stream = stream.dup - stream.reopen(IO::NULL) - stream.sync = true - yield - ensure - stream.reopen(old_stream) - old_stream.close - end end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 6ea8b93be7..51b0034755 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1,3 +1,4 @@ +require 'active_support/testing/stream' require "cases/helper" require "cases/migration/helper" require 'bigdecimal/util' @@ -14,9 +15,9 @@ require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers" class BigNumber < ActiveRecord::Base unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) - attribute :value_of_e, Type::Integer.new + attribute :value_of_e, :integer end - attribute :my_house_population, Type::Integer.new + attribute :my_house_population, :integer end class Reminder < ActiveRecord::Base; end @@ -721,6 +722,8 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end class CopyMigrationsTest < ActiveRecord::TestCase + include ActiveSupport::Testing::Stream + def setup end @@ -930,23 +933,4 @@ class CopyMigrationsTest < ActiveRecord::TestCase ActiveRecord::Base.logger = old end - private - - def quietly - silence_stream(STDOUT) do - silence_stream(STDERR) do - yield - end - end - end - - def silence_stream(stream) - old_stream = stream.dup - stream.reopen(IO::NULL) - stream.sync = true - yield - ensure - stream.reopen(old_stream) - old_stream.close - end end diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index 4aaf6f8b5f..ae18573126 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -199,6 +199,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + Topic.reset_column_information attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" @@ -209,6 +210,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time assert_equal Time.zone, topic.written_on.time_zone end + ensure + Topic.reset_column_information end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false @@ -227,6 +230,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do Topic.skip_time_zone_conversion_for_attributes = [:written_on] + Topic.reset_column_information attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" @@ -238,12 +242,14 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end ensure Topic.skip_time_zone_conversion_for_attributes = [] + Topic.reset_column_information end # Oracle does not have a TIME datatype. unless current_adapter?(:OracleAdapter) def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + Topic.reset_column_information attributes = { "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" @@ -253,6 +259,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time assert_not topic.bonus_time.utc? end + ensure + Topic.reset_column_information end end diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 15c60d5562..f9bc266e84 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -93,14 +93,14 @@ class MultipleDbTest < ActiveRecord::TestCase assert_not_equal Entrant.arel_engine.connection, Course.arel_engine.connection end - def test_count_on_custom_connection - ActiveRecord::Base.remove_connection - assert_equal 1, College.count - ensure - ActiveRecord::Base.establish_connection :arunit - end - unless in_memory_db? + def test_count_on_custom_connection + ActiveRecord::Base.remove_connection + assert_equal 1, College.count + ensure + ActiveRecord::Base.establish_connection :arunit + end + def test_associations_should_work_when_model_has_no_connection begin ActiveRecord::Base.remove_connection diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 537937decd..6af31017d6 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -5,6 +5,7 @@ require "models/cake_designer" require "models/chef" require "models/comment" require "models/edge" +require "models/essay" require "models/post" require "models/price_estimate" require "models/topic" @@ -13,7 +14,7 @@ require "models/vertex" module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors, :binaries + fixtures :posts, :edges, :authors, :binaries, :essays def test_where_copies_bind_params author = authors(:david) @@ -240,5 +241,40 @@ module ActiveRecord count = Binary.where(:data => 0).count assert_equal 0, count end + + def test_where_on_association_with_custom_primary_key + author = authors(:david) + essay = Essay.where(writer: author).first + + assert_equal essays(:david_modest_proposal), essay + end + + def test_where_on_association_with_custom_primary_key_with_relation + author = authors(:david) + essay = Essay.where(writer: Author.where(id: author.id)).first + + assert_equal essays(:david_modest_proposal), essay + end + + def test_where_on_association_with_relation_performs_subselect_not_two_queries + author = authors(:david) + + assert_queries(1) do + Essay.where(writer: Author.where(id: author.id)).to_a + end + end + + def test_where_on_association_with_custom_primary_key_with_array_of_base + author = authors(:david) + essay = Essay.where(writer: [author]).first + + assert_equal essays(:david_modest_proposal), essay + end + + def test_where_on_association_with_custom_primary_key_with_array_of_ids + essay = Essay.where(writer: ["David"]).first + + assert_equal essays(:david_modest_proposal), essay + end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 194faa9473..c9c05b75a6 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -168,6 +168,22 @@ module ActiveRecord assert_raises(ArgumentError) { Relation::HashMerger.new(nil, omg: 'lol') } end + test 'merging nil or false raises' do + relation = Relation.new(FakeKlass, :b, nil) + + e = assert_raises(ArgumentError) do + relation = relation.merge nil + end + + assert_equal 'invalid argument: nil.', e.message + + e = assert_raises(ArgumentError) do + relation = relation.merge false + end + + assert_equal 'invalid argument: false.', e.message + end + test '#values returns a dup of the values' do relation = Relation.new(FakeKlass, :b, nil).where! :foo values = relation.values diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index c8ea702488..bafc9fa81b 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -73,10 +73,10 @@ class SchemaDumperTest < ActiveRecord::TestCase next if column_set.empty? lengths = column_set.map do |column| - if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid|point)\s+"/) + if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|xml|uuid|point)\s+"/) match[0].length end - end + end.compact assert_equal 1, lengths.uniq.length end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 5ba17359f0..e0b01ae8e0 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -1,10 +1,13 @@ require 'active_support/test_case' +require 'active_support/testing/stream' module ActiveRecord # = Active Record Test Case # # Defines some test assertions to test against SQL queries. class TestCase < ActiveSupport::TestCase #:nodoc: + include ActiveSupport::Testing::Stream + def teardown SQLCounter.clear_log end @@ -13,23 +16,6 @@ module ActiveRecord assert_equal expected.to_s, actual.to_s, message end - def capture(stream) - stream = stream.to_s - captured_stream = Tempfile.new(stream) - stream_io = eval("$#{stream}") - origin_stream = stream_io.dup - stream_io.reopen(captured_stream) - - yield - - stream_io.rewind - return captured_stream.read - ensure - captured_stream.close - captured_stream.unlink - stream_io.reopen(origin_stream) - end - def capture_sql SQLCounter.clear_log yield diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb index 0c60f0690c..1e836f2142 100644 --- a/activerecord/test/cases/type/integer_test.rb +++ b/activerecord/test/cases/type/integer_test.rb @@ -113,7 +113,7 @@ module ActiveRecord test "values which are out of range can be re-assigned" do klass = Class.new(ActiveRecord::Base) do self.table_name = 'posts' - attribute :foo, Type::Integer.new + attribute :foo, :integer end model = klass.new diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index b0f34e5f47..f4f316f393 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -150,7 +150,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_numericality_validation_with_mutation Topic.class_eval do - attribute :wibble, ActiveRecord::Type::String.new + attribute :wibble, :string validates_numericality_of :wibble, only_integer: true end |