diff options
Diffstat (limited to 'activerecord')
48 files changed, 457 insertions, 370 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 1470c6dec1..16f3f47d29 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,36 @@ +* Allow `:precision` option for time type columns. + + *Ryuta Kamizono* + +* Have `enum` perform type casting consistently with the rest of Active + Record, such as `where`. + + *Sean Griffin* + +* `scoping` no longer pollutes the current scope of sibling classes when using + STI. e.x. + + StiOne.none.scoping do + StiTwo.all + end + + Fixes #18806. + + *Sean Griffin* + +* `remove_reference` with `foreign_key: true` removes the foreign key before + removing the column. This fixes a bug where it was not possible to remove + the column on MySQL. + + Fixes #18664. + + *Yves Senn* + +* `find_in_batches` now accepts an `:end_at` parameter that complements the `:start` + parameter to specify where to stop batch processing. + + *Vipul A M* + * Fix rounding problem for PostgreSQL timestamp column. If timestamp column have the precision, it need to format according to @@ -20,7 +53,7 @@ *Rafael Mendonça França* -* Use `SCHEMA` instead of `DB_STRUCTURE` for specifiying structure file. +* Use `SCHEMA` instead of `DB_STRUCTURE` for specifying structure file. This makes the db:structure tasks consistent with test:load_structure. @@ -104,7 +137,7 @@ *Sean Griffin* * Values which would error while being sent to the database (such as an - ASCII-8BIT string with invalid UTF-8 bytes on Sqlite3), no longer error on + ASCII-8BIT string with invalid UTF-8 bytes on SQLite3), no longer error on assignment. They will still error when sent to the database, but you are given the ability to re-assign it to a valid value. diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 81a42e22f3..5a3b4f0c40 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1014,7 +1014,7 @@ module ActiveRecord # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. - # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for + # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete # the join records, without running their callbacks). # diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 0f3c285a80..e85777c335 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -20,7 +20,7 @@ module ActiveRecord end class LazyAttributeHash # :nodoc: - delegate :select, :transform_values, :each_key, to: :materialize + delegate :transform_values, :each_key, to: :materialize def initialize(types, values, additional_types) @types = types @@ -50,6 +50,16 @@ module ActiveRecord super end + def select + keys = types.keys | values.keys | delegate_hash.keys + keys.each_with_object({}) do |key, hash| + attribute = self[key] + if yield(key, attribute) + hash[key] = attribute + end + end + end + protected attr_reader :types, :values, :additional_types, :delegate_hash diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 3b1e321f4b..42ad285340 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -201,16 +201,14 @@ module ActiveRecord # isolation level. However, support is disabled for MySQL versions below 5, # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] # which means the isolation level gets persisted outside the transaction. - def transaction(options = {}) - options.assert_valid_keys :requires_new, :joinable, :isolation - - if !options[:requires_new] && current_transaction.joinable? - if options[:isolation] + def transaction(requires_new: nil, isolation: nil, joinable: true) + if !requires_new && current_transaction.joinable? + if isolation raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" end yield else - transaction_manager.within_new_transaction(options) { yield } + transaction_manager.within_new_transaction(isolation: isolation, joinable: joinable) { yield } end rescue ActiveRecord::Rollback # rollbacks are silently swallowed diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 55d3360070..947e11c7bf 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -102,6 +102,11 @@ module ActiveRecord quote_table_name("#{table}.#{attr}") end + def quote_default_expression(value, column) #:nodoc: + value = lookup_cast_type(column.sql_type).type_cast_for_database(value) + quote(value) + end + def quoted_true "'t'" end @@ -127,7 +132,12 @@ module ActiveRecord end end - value.to_s(:db) + result = value.to_s(:db) + if value.respond_to?(:usec) && value.usec > 0 + "#{result}.#{sprintf("%06d", value.usec)}" + else + result + end end def prepare_binds_for_database(binds) # :nodoc: 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..bc8fa9b6cf 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 @@ -98,8 +98,7 @@ module ActiveRecord end def quote_default_expression(value, column) - value = type_for_column(column).type_cast_for_database(value) - @conn.quote(value) + @conn.quote_default_expression(value, column) end def options_include_default?(options) @@ -118,10 +117,6 @@ module ActiveRecord MSG end end - - def type_for_column(column) - @conn.lookup_cast_type(column.sql_type) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 7eaa89c9a7..8cd4b8e5b2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,7 +15,7 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type, :cast_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index ed32997d25..c9180f64db 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -662,7 +662,13 @@ module ActiveRecord # # remove_reference(:products, :supplier, polymorphic: true) # + # ====== Remove the reference with a foreign key + # + # remove_reference(:products, :user, index: true, foreign_key: true) + # def remove_reference(table_name, ref_name, options = {}) + remove_foreign_key table_name, ref_name if options[:foreign_key] + remove_column(table_name, "#{ref_name}_id") remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] end @@ -834,6 +840,12 @@ module ActiveRecord raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified" end + elsif [:datetime, :time].include?(type) && precision ||= native[:precision] + if (0..6) === precision + column_type_sql << "(#{precision})" + else + raise(ActiveRecordError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6") + end elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit]) column_type_sql << "(#{limit})" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 9392bcb473..c307189980 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -245,6 +245,11 @@ module ActiveRecord false end + # Does this adapter support datetime with precision? + def supports_datetime_with_precision? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -263,18 +268,6 @@ module ActiveRecord {} end - # QUOTING ================================================== - - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.acts_like?(:time) && value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - # Returns a bind substitution value given a bind +column+ # NOTE: The column param is currently being used by the sqlserver-adapter def substitute_at(column, _unused = 0) @@ -409,15 +402,15 @@ module ActiveRecord protected def initialize_type_map(m) # :nodoc: - register_class_with_limit m, %r(boolean)i, Type::Boolean - register_class_with_limit m, %r(char)i, Type::String - register_class_with_limit m, %r(binary)i, Type::Binary - register_class_with_limit m, %r(text)i, Type::Text - register_class_with_limit m, %r(date)i, Type::Date - register_class_with_limit m, %r(time)i, Type::Time - register_class_with_limit m, %r(datetime)i, Type::DateTime - register_class_with_limit m, %r(float)i, Type::Float - register_class_with_limit m, %r(int)i, Type::Integer + register_class_with_limit m, %r(boolean)i, Type::Boolean + register_class_with_limit m, %r(char)i, Type::String + register_class_with_limit m, %r(binary)i, Type::Binary + register_class_with_limit m, %r(text)i, Type::Text + register_class_with_precision m, %r(date)i, Type::Date + register_class_with_precision m, %r(time)i, Type::Time + register_class_with_precision m, %r(datetime)i, Type::DateTime + register_class_with_limit m, %r(float)i, Type::Float + register_class_with_limit m, %r(int)i, Type::Integer m.alias_type %r(blob)i, 'binary' m.alias_type %r(clob)i, 'text' @@ -451,6 +444,13 @@ module ActiveRecord end end + def register_class_with_precision(mapping, key, klass) # :nodoc: + mapping.register_type(key) do |*args| + precision = extract_precision(args.last) + klass.new(precision: precision) + end + end + def extract_scale(sql_type) # :nodoc: case sql_type when /\((\d+)\)/ then 0 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 c29692d6ca..0a9e599c3c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -73,6 +73,12 @@ module ActiveRecord spec end + def prepare_column_options(column) + spec = super + spec.delete(:precision) if column.type == :datetime && column.precision == 0 + spec + end + class Column < ConnectionAdapters::Column # :nodoc: delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true @@ -245,6 +251,10 @@ module ActiveRecord version[0] >= 5 end + def supports_datetime_with_precision? + (version[0] == 5 && version[1] >= 6) || version[0] >= 6 + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -617,13 +627,6 @@ module ActiveRecord when 0x1000000..0xffffffff; 'longtext' else raise(ActiveRecordError, "No text type has character length #{limit}") end - when 'datetime' - return super unless precision - - case precision - when 0..6; "datetime(#{precision})" - else raise(ActiveRecordError, "No datetime type has precision of #{precision}. The allowed range of precision is from 0 to 6.") - end else super end @@ -712,11 +715,6 @@ module ActiveRecord m.alias_type %r(year)i, 'integer' m.alias_type %r(bit)i, 'binary' - m.register_type(%r(datetime)i) do |sql_type| - precision = extract_precision(sql_type) - MysqlDateTime.new(precision: precision) - end - m.register_type(%r(enum)i) do |sql_type| limit = sql_type[/^enum\((.+)\)/i, 1] .split(',').map{|enum| enum.strip.length - 2}.max @@ -734,6 +732,14 @@ module ActiveRecord end end + def extract_precision(sql_type) + if /datetime/ === sql_type + super || 0 + else + super + end + end + def fetch_type_metadata(sql_type, collation = "", extra = "") MysqlTypeMetadata.new(super(sql_type), collation: collation, extra: extra, strict: strict_mode?) end @@ -916,14 +922,6 @@ module ActiveRecord TableDefinition.new(native_database_types, name, temporary, options, as) end - class MysqlDateTime < Type::DateTime # :nodoc: - private - - def has_precision? - precision || 0 - end - end - class MysqlString < Type::String # :nodoc: def type_cast_for_database(value) case value @@ -945,7 +943,7 @@ module ActiveRecord end def type_classes_with_standard_constructor - super.merge(string: MysqlString, date_time: MysqlDateTime) + super.merge(string: MysqlString) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index d28a2b4fa0..92349e2f9b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,25 +1,19 @@ -require 'active_record/connection_adapters/postgresql/oid/infinity' - require 'active_record/connection_adapters/postgresql/oid/array' require 'active_record/connection_adapters/postgresql/oid/bit' require 'active_record/connection_adapters/postgresql/oid/bit_varying' require 'active_record/connection_adapters/postgresql/oid/bytea' require 'active_record/connection_adapters/postgresql/oid/cidr' -require 'active_record/connection_adapters/postgresql/oid/date' require 'active_record/connection_adapters/postgresql/oid/date_time' require 'active_record/connection_adapters/postgresql/oid/decimal' require 'active_record/connection_adapters/postgresql/oid/enum' -require 'active_record/connection_adapters/postgresql/oid/float' require 'active_record/connection_adapters/postgresql/oid/hstore' require 'active_record/connection_adapters/postgresql/oid/inet' -require 'active_record/connection_adapters/postgresql/oid/integer' require 'active_record/connection_adapters/postgresql/oid/json' require 'active_record/connection_adapters/postgresql/oid/jsonb' require 'active_record/connection_adapters/postgresql/oid/money' require 'active_record/connection_adapters/postgresql/oid/point' require 'active_record/connection_adapters/postgresql/oid/range' require 'active_record/connection_adapters/postgresql/oid/specialized_string' -require 'active_record/connection_adapters/postgresql/oid/time' require 'active_record/connection_adapters/postgresql/oid/uuid' require 'active_record/connection_adapters/postgresql/oid/vector' require 'active_record/connection_adapters/postgresql/oid/xml' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb deleted file mode 100644 index 1d8d264530..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Date < Type::Date # :nodoc: - include Infinity - end - end - end - end -end 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 2fe61eeb77..2c04c46131 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 @@ -3,17 +3,6 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: 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/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb deleted file mode 100644 index 78ef94b912..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Float < Type::Float # :nodoc: - include Infinity - - def cast_value(value) - case value - when ::Float then value - when 'Infinity' then ::Float::INFINITY - when '-Infinity' then -::Float::INFINITY - when 'NaN' then ::Float::NAN - else value.to_f - end - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb deleted file mode 100644 index e47780399a..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - module Infinity # :nodoc: - def infinity(options = {}) - options[:negative] ? -::Float::INFINITY : ::Float::INFINITY - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb deleted file mode 100644 index 59abdc0009..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Integer < Type::Integer # :nodoc: - include Infinity - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index df890c2ed6..2163674019 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -3,8 +3,6 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Money < Type::Decimal # :nodoc: - include Infinity - class_attribute :precision def type 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 2a5a59fbc6..9d3633d109 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -59,13 +59,23 @@ module ActiveRecord def extract_bounds(value) from, to = value[1..-2].split(',') { - from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, - to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, + from: (value[1] == ',' || from == '-infinity') ? infinity(negative: true) : from, + to: (value[-2] == ',' || to == 'infinity') ? infinity : to, exclude_start: (value[0] == '('), exclude_end: (value[-1] == ')') } end + def infinity(negative: false) + if subtype.respond_to?(:infinity) + subtype.infinity(negative: negative) + elsif negative + -::Float::INFINITY + else + ::Float::INFINITY + end + end + def infinity?(value) value.respond_to?(:infinite?) && value.infinite? end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb deleted file mode 100644 index 8f0246eddb..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Time < Type::Time # :nodoc: - include Infinity - end - 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 11114f32fe..b8d0e26f85 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -52,12 +52,14 @@ module ActiveRecord end # Does not quote function default values for UUID columns - def quote_default_value(value, column) #:nodoc: + def quote_default_expression(value, column) #:nodoc: if column.type == :uuid && value =~ /\(\)/ value - else + elsif column.respond_to?(:array?) value = type_cast_from_column(column, value) quote(value) + else + super end end @@ -127,18 +129,15 @@ module ActiveRecord 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, 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..503dda60f4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -4,34 +4,11 @@ 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 - - def quote_default_expression(value, column) - if column.type == :uuid && value =~ /\(\)/ - value - else - 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 @@ -455,7 +432,7 @@ module ActiveRecord # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". execute alter_column_query % "DROP DEFAULT" else - execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}" + execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}" end end @@ -463,7 +440,7 @@ module ActiveRecord clear_cache! unless null || default.nil? column = column_for(table_name, column_name) - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column end execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") end @@ -557,13 +534,6 @@ module ActiveRecord when 5..8; 'bigint' else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") end - when 'datetime' - return super unless precision - - case precision - when 0..6; "timestamp(#{precision})" - else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") - end else super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e8ecaffcab..6d7e1075d7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -180,6 +180,10 @@ module ActiveRecord true end + def supports_datetime_with_precision? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -255,6 +259,8 @@ module ActiveRecord @table_alias_length = nil connect + add_pg_decoders + @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) @@ -459,11 +465,11 @@ module ActiveRecord end def initialize_type_map(m) # :nodoc: - register_class_with_limit m, 'int2', OID::Integer - register_class_with_limit m, 'int4', OID::Integer - register_class_with_limit m, 'int8', OID::Integer + register_class_with_limit m, 'int2', Type::Integer + register_class_with_limit m, 'int4', Type::Integer + register_class_with_limit m, 'int8', Type::Integer m.alias_type 'oid', 'int2' - m.register_type 'float4', OID::Float.new + m.register_type 'float4', Type::Float.new m.alias_type 'float8', 'float4' m.register_type 'text', Type::Text.new register_class_with_limit m, 'varchar', Type::String @@ -474,8 +480,7 @@ module ActiveRecord register_class_with_limit m, 'bit', OID::Bit register_class_with_limit m, 'varbit', OID::BitVarying m.alias_type 'timestamptz', 'timestamp' - m.register_type 'date', OID::Date.new - m.register_type 'time', OID::Time.new + m.register_type 'date', Type::Date.new m.register_type 'money', OID::Money.new m.register_type 'bytea', OID::Bytea.new @@ -501,10 +506,8 @@ module ActiveRecord m.alias_type 'lseg', 'varchar' m.alias_type 'box', 'varchar' - m.register_type 'timestamp' do |_, _, sql_type| - precision = extract_precision(sql_type) - OID::DateTime.new(precision: precision) - end + register_class_with_precision m, 'time', Type::Time + register_class_with_precision m, 'timestamp', OID::DateTime m.register_type 'numeric' do |_, fmod, sql_type| precision = extract_precision(sql_type) @@ -780,6 +783,36 @@ module ActiveRecord end end end + + def add_pg_decoders + coders_by_name = { + 'int2' => PG::TextDecoder::Integer, + 'int4' => PG::TextDecoder::Integer, + 'int8' => PG::TextDecoder::Integer, + 'oid' => PG::TextDecoder::Integer, + 'float4' => PG::TextDecoder::Float, + 'float8' => PG::TextDecoder::Float, + 'bool' => PG::TextDecoder::Boolean, + } + query = <<-SQL + SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype + FROM pg_type as t + SQL + coders = execute_and_clear(query, "SCHEMA", []) do |result| + result + .map { |row| construct_coder(row, coders_by_name['typname']) } + .compact + end + + map = PG::TypeMapByOid.new + coders.each { |coder| map.add_coder(coder) } + @connection.type_map_for_results = map + end + + def construct_coder(row, coder_class) + return unless coder_class + coder_class.new(oid: row['oid'], name: row['typname']) + 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 edd060248f..400b586c95 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -41,15 +41,6 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Binary < Type::Binary # :nodoc: - def cast_value(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value - end - end - # The SQLite3 adapter works SQLite 3.6.16 or newer # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). # @@ -240,10 +231,6 @@ 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 @@ -493,11 +480,6 @@ module ActiveRecord protected - def initialize_type_map(m) - super - m.register_type(/binary/i, SQLite3Binary.new) - end - def table_structure(table_name) structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index f053372cfb..3086f13f96 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -79,6 +79,37 @@ module ActiveRecord super end + class EnumType < Type::Value + def initialize(name, mapping) + @name = name + @mapping = mapping + end + + def type_cast_from_user(value) + return if value.blank? + + if mapping.has_key?(value) + value.to_s + elsif mapping.has_value?(value) + mapping.key(value) + else + raise ArgumentError, "'#{value}' is not a valid #{name}" + end + end + + def type_cast_from_database(value) + mapping.key(value.to_i) + end + + def type_cast_for_database(value) + mapping.fetch(value, value) + end + + protected + + attr_reader :name, :mapping + end + def enum(definitions) klass = self definitions.each do |name, values| @@ -90,37 +121,19 @@ module ActiveRecord detect_enum_conflict!(name, name.to_s.pluralize, true) klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } - _enum_methods_module.module_eval do - # def status=(value) self[:status] = statuses[value] end - klass.send(:detect_enum_conflict!, name, "#{name}=") - define_method("#{name}=") { |value| - if enum_values.has_key?(value) || value.blank? - self[name] = enum_values[value] - elsif enum_values.has_value?(value) - # Assigning a value directly is not a end-user feature, hence it's not documented. - # This is used internally to make building objects from the generated scopes work - # as expected, i.e. +Conversation.archived.build.archived?+ should be true. - self[name] = value - else - raise ArgumentError, "'#{value}' is not a valid #{name}" - end - } - - # def status() statuses.key self[:status] end - klass.send(:detect_enum_conflict!, name, name) - define_method(name) { enum_values.key self[name] } + detect_enum_conflict!(name, name) + detect_enum_conflict!(name, "#{name}=") - # def status_before_type_cast() statuses.key self[:status] end - klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast") - define_method("#{name}_before_type_cast") { enum_values.key self[name] } + attribute name, EnumType.new(name, enum_values) + _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index pairs.each do |value, i| enum_values[value] = i # def active?() status == 0 end klass.send(:detect_enum_conflict!, name, "#{value}?") - define_method("#{value}?") { self[name] == i } + define_method("#{value}?") { self[name] == value.to_s } # def active!() update! status: :active end klass.send(:detect_enum_conflict!, name, "#{value}!") @@ -128,7 +141,7 @@ module ActiveRecord # scope :active, -> { where status: 0 } klass.send(:detect_enum_conflict!, name, value, true) - klass.scope value, -> { klass.where name => i } + klass.scope value, -> { klass.where name => value } end end defined_enums[name.to_s] = enum_values @@ -138,25 +151,7 @@ module ActiveRecord private def _enum_methods_module @_enum_methods_module ||= begin - mod = Module.new do - private - def save_changed_attribute(attr_name, old) - if (mapping = self.class.defined_enums[attr_name.to_s]) - value = _read_attribute(attr_name) - if attribute_changed?(attr_name) - if mapping[old] == value - clear_attribute_changes([attr_name]) - end - else - if old != value - set_attribute_was(attr_name, mapping.key(old)) - end - end - else - super - end - end - end + mod = Module.new include mod mod 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 dde4dfa83c..2591e7492d 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -286,7 +286,7 @@ db_namespace = namespace :db do end desc "Recreate the databases from the structure.sql file" - task :load => [:environment, :load_config] do + task :load => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA']) end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 9d690af11d..a543341149 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -27,11 +27,12 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the primary key value to start from. + # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:start+ option on that worker). + # (by setting the +:start+ and +:end_at+ option on each worker). # # # Let's process for a batch of 2000 records, skipping the first 2000 rows # Person.find_each(start: 2000, batch_size: 2000) do |person| @@ -45,14 +46,15 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_each(start: nil, batch_size: 1000) + def find_each(start: nil, end_at: nil, batch_size: 1000) if block_given? - find_in_batches(start: start, batch_size: batch_size) do |records| + find_in_batches(start: start, end_at: end_at, batch_size: batch_size) do |records| records.each { |record| yield record } end else - enum_for(:find_each, start: start, batch_size: batch_size) do - start ? where(table[primary_key].gteq(start)).size : size + enum_for(:find_each, start: start, end_at: end_at, batch_size: batch_size) do + relation = self + apply_limits(relation, start, end_at).size end end end @@ -77,11 +79,12 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the primary key value to start from. + # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:start+ option on that worker). + # (by setting the +:start+ and +:end_at+ option on each worker). # # # Let's process the next 2000 records # Person.find_in_batches(start: 2000, batch_size: 2000) do |group| @@ -95,12 +98,12 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_in_batches(start: nil, batch_size: 1000) + def find_in_batches(start: nil, end_at: nil, batch_size: 1000) relation = self unless block_given? - return to_enum(:find_in_batches, start: start, batch_size: batch_size) do - total = start ? where(table[primary_key].gteq(start)).size : size + return to_enum(:find_in_batches, start: start, end_at: end_at, batch_size: batch_size) do + total = apply_limits(relation, start, end_at).size (total - 1).div(batch_size) + 1 end end @@ -110,7 +113,8 @@ module ActiveRecord end relation = relation.reorder(batch_order).limit(batch_size) - records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a + relation = apply_limits(relation, start, end_at) + records = relation.to_a while records.any? records_size = records.size @@ -127,6 +131,12 @@ module ActiveRecord private + def apply_limits(relation, start, end_at) + relation = relation.where(table[primary_key].gteq(start)) if start + relation = relation.where(table[primary_key].lteq(end_at)) if end_at + relation + end + def batch_order "#{quoted_table_name}.#{quoted_primary_key} ASC" end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 3e43591672..fca4f1c9d3 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -11,11 +11,11 @@ module ActiveRecord module ClassMethods def current_scope #:nodoc: - ScopeRegistry.value_for(:current_scope, base_class.to_s) + ScopeRegistry.value_for(:current_scope, self.to_s) end def current_scope=(scope) #:nodoc: - ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope) + ScopeRegistry.set_value_for(:current_scope, self.to_s, scope) end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index a083deafe1..43c7b1c574 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -157,11 +157,20 @@ module ActiveRecord extension = Module.new(&block) if block - singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { body.call(*args) } - scope = scope.extending(extension) if extension + if body.respond_to?(:to_proc) + singleton_class.send(:define_method, name) do |*args| + scope = all.scoping { instance_exec(*args, &body) } + scope = scope.extending(extension) if extension - scope || all + scope || all + end + else + singleton_class.send(:define_method, name) do |*args| + scope = all.scoping { body.call(*args) } + scope = scope.extending(extension) if extension + + scope || all + 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 e8614b16e0..a25f2521bb 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -11,28 +11,25 @@ module ActiveRecord 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.respond_to?(zone_conversion_method) - value = value.send(zone_conversion_method) + if precision && value.respond_to?(:usec) + number_of_insignificant_digits = 6 - precision + round_power = 10 ** number_of_insignificant_digits + value = value.change(usec: value.usec / round_power * round_power) end - return value unless has_precision? + if value.acts_like?(:time) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal - 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 - result + if value.respond_to?(zone_conversion_method) + value = value.send(zone_conversion_method) + end end + + value end private - alias has_precision? precision - def cast_value(string) return string unless string.is_a?(::String) return if string.empty? diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb index 0a9088e0a1..b3928242b7 100644 --- a/activerecord/lib/active_record/type/float.rb +++ b/activerecord/lib/active_record/type/float.rb @@ -12,7 +12,13 @@ module ActiveRecord private def cast_value(value) - value.to_f + case value + when ::Float then value + when "Infinity" then ::Float::INFINITY + when "-Infinity" then -::Float::INFINITY + when "NaN" then ::Float::NAN + else value.to_f + end end end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index d261e2db55..ff8ce93248 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -122,11 +122,4 @@ class MysqlConnectionTest < ActiveRecord::TestCase ensure @connection.execute "DROP TABLE `bar_baz`" end - - if mysql_56? - def test_quote_time_usec - assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0)) - assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0).to_datetime) - end - end end diff --git a/activerecord/test/cases/adapters/mysql2/datetime_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_test.rb index ae00f4e131..7a37247d6a 100644 --- a/activerecord/test/cases/adapters/mysql2/datetime_test.rb +++ b/activerecord/test/cases/adapters/mysql2/datetime_test.rb @@ -4,15 +4,12 @@ if mysql_56? class DateTimeTest < ActiveRecord::TestCase self.use_transactional_fixtures = false - class Foo < ActiveRecord::Base; end - - def test_default_datetime_precision - ActiveRecord::Base.connection.create_table(:foos, force: true) - ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime - ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime - assert_nil activerecord_column_option('foos', 'created_at', 'precision') + teardown do + ActiveRecord::Base.connection.drop_table(:foos, if_exists: true) end + class Foo < ActiveRecord::Base; end + def test_datetime_data_type_with_precision ActiveRecord::Base.connection.create_table(:foos, force: true) ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, precision: 1 diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index cbe7e62870..3b123f979e 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- require "cases/helper" require 'support/schema_dumping_helper' @@ -13,20 +14,18 @@ module PostgresqlJSONSharedTestCases def setup @connection = ActiveRecord::Base.connection begin - @connection.transaction do - @connection.create_table('json_data_type') do |t| - t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {} - t.public_send column_type, 'settings' # t.json 'settings' - end + @connection.create_table('json_data_type') do |t| + t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {} + t.public_send column_type, 'settings' # t.json 'settings' end rescue ActiveRecord::StatementInvalid - skip "do not test on PG without json" + skip "do not test on PostgreSQL without #{column_type} type." end @column = JsonDataType.columns_hash['payload'] end def teardown - @connection.execute 'drop table if exists json_data_type' + @connection.drop_table :json_data_type, if_exists: true end def test_column diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 894cf1ffa2..60baacf67a 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -27,11 +27,6 @@ module ActiveRecord assert_equal "'Infinity'", @conn.quote(infinity) end - def test_quote_time_usec - assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0)) - assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime) - end - def test_quote_range range = "1,2]'; SELECT * FROM users; --".."a" type = OID::Range.new(Type::Integer.new, :int8range) diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index 8246b14b93..3091ee136f 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'support/schema_dumping_helper' require 'models/developer' require 'models/topic' @@ -46,8 +47,6 @@ 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 @@ -72,6 +71,35 @@ class TimestampTest < ActiveRecord::TestCase assert_equal(-1.0 / 0.0, d.updated_at) end + def test_bc_timestamp + date = Date.new(0) - 1.week + Developer.create!(:name => "aaron", :updated_at => date) + assert_equal date, Developer.find_by_name("aaron").updated_at + end + + def test_bc_timestamp_leap_year + date = Time.utc(-4, 2, 29) + Developer.create!(:name => "taihou", :updated_at => date) + assert_equal date, Developer.find_by_name("taihou").updated_at + end + + def test_bc_timestamp_year_zero + date = Time.utc(0, 4, 7) + Developer.create!(:name => "yahagi", :updated_at => date) + assert_equal date, Developer.find_by_name("yahagi").updated_at + end +end + +class TimestampPrecisionTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_fixtures = false + + class Foo < ActiveRecord::Base; end + + teardown do + ActiveRecord::Base.connection.drop_table(:foos, if_exists: true) + end + def test_default_datetime_precision ActiveRecord::Base.connection.create_table(:foos) ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime @@ -119,24 +147,6 @@ class TimestampTest < ActiveRecord::TestCase assert_equal '4', pg_datetime_precision('foos', 'updated_at') end - def test_bc_timestamp - date = Date.new(0) - 1.week - Developer.create!(:name => "aaron", :updated_at => date) - assert_equal date, Developer.find_by_name("aaron").updated_at - end - - def test_bc_timestamp_leap_year - date = Time.utc(-4, 2, 29) - Developer.create!(:name => "taihou", :updated_at => date) - assert_equal date, Developer.find_by_name("taihou").updated_at - end - - def test_bc_timestamp_year_zero - date = Time.utc(0, 4, 7) - Developer.create!(:name => "yahagi", :updated_at => date) - 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 @@ -145,12 +155,22 @@ class TimestampTest < ActiveRecord::TestCase 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 1, Foo.where(updated_at: date).count 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 + def test_datetime_precision_with_zero_should_be_dumped + ActiveRecord::Base.connection.create_table(:foos) do |t| + t.timestamps precision: 0 + end + output = dump_table_schema("foos") + assert_match %r{t\.datetime\s+"created_at",\s+precision: 0,\s+null: false$}, output + assert_match %r{t\.datetime\s+"updated_at",\s+precision: 0,\s+null: false$}, output + end + private def pg_datetime_precision(table_name, column_name) diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index 8025c7c4d2..112cd2fb14 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -65,6 +65,16 @@ module ActiveRecord assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h) end + test "to_hash maintains order" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '2.2', bar: '3.3') + + attributes[:bar] + hash = attributes.to_h + + assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a + end + test "values_before_type_cast" do builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) attributes = builder.build_from_database(foo: '1.1', bar: '2.2') diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index c12fa03015..c05382598b 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -106,6 +106,15 @@ class EachTest < ActiveRecord::TestCase end end + def test_find_in_batches_should_end_at_the_end_option + assert_queries(6) do + Post.find_in_batches(batch_size: 1, end_at: 5) do |batch| + assert_kind_of Array, batch + assert_kind_of Post, batch.first + end + end + end + def test_find_in_batches_shouldnt_execute_query_unless_needed assert_queries(2) do Post.find_in_batches(:batch_size => @total) {|batch| assert_kind_of Array, batch } diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 346fcab6ea..ed568413a2 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -26,6 +26,17 @@ class EnumTest < ActiveRecord::TestCase assert_equal @book, Book.unread.first end + test "build from scope" do + assert Book.proposed.build.proposed? + refute Book.proposed.build.written? + assert Book.where(status: Book.statuses[:proposed]).build.proposed? + end + + test "find via where" do + assert_equal @book, Book.where(status: "proposed").first + refute_equal @book, Book.where(status: "written").first + end + test "update by declaration" do @book.written! assert @book.written? @@ -161,7 +172,11 @@ class EnumTest < ActiveRecord::TestCase end test "_before_type_cast returns the enum label (required for form fields)" do - assert_equal "proposed", @book.status_before_type_cast + if @book.status_came_from_user? + assert_equal "proposed", @book.status_before_type_cast + else + assert_equal "proposed", @book.status + end end test "reserved enum names" do diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 0a577fa2f5..f1f927852c 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -124,7 +124,7 @@ def enable_extension!(extension, connection) return connection.reconnect! if connection.extension_enabled?(extension) connection.enable_extension extension - connection.commit_db_transaction + connection.commit_db_transaction if connection.transaction_open? connection.reconnect! end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 8cba777fe2..3844b1a92e 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -256,6 +256,11 @@ module ActiveRecord assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }], nil], add end + def test_invert_remove_reference_with_index_and_foreign_key + add = @recorder.inverse_of :remove_reference, [:table, :taggable, { index: true, foreign_key: true }] + assert_equal [:add_reference, [:table, :taggable, { index: true, foreign_key: true }], nil], add + end + def test_invert_remove_belongs_to_alias add = @recorder.inverse_of :remove_belongs_to, [:table, :user] assert_equal [:add_reference, [:table, :user], nil], add diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 66e2175c48..b2f2d077eb 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -1,5 +1,4 @@ require 'cases/helper' -require 'active_support/testing/stream' require 'support/ddl_helper' require 'support/schema_dumping_helper' diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 99de7db70c..17ac72a109 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -10,8 +10,8 @@ module ActiveRecord end teardown do - @connection.drop_table("testings") if @connection.table_exists? "testings" - @connection.drop_table("testing_parents") if @connection.table_exists? "testing_parents" + @connection.drop_table "testings", if_exists: true + @connection.drop_table "testing_parents", if_exists: true end test "foreign keys can be created with the table" do @@ -95,6 +95,16 @@ module ActiveRecord end end end + + test "foreign key column can be removed" do + @connection.create_table :testings do |t| + t.references :testing_parent, index: true, foreign_key: true + end + + assert_difference "@connection.foreign_keys('testings').size", -1 do + @connection.remove_reference :testings, :testing_parent, foreign_key: true + end + end end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 51b0034755..5f9fd5d527 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1,4 +1,3 @@ -require 'active_support/testing/stream' require "cases/helper" require "cases/migration/helper" require 'bigdecimal/util' @@ -120,10 +119,6 @@ class MigrationTest < ActiveRecord::TestCase end def test_create_table_with_force_true_does_not_drop_nonexisting_table - if Person.connection.table_exists?(:testings2) - Person.connection.drop_table :testings2 - end - # using a copy as we need the drop_table method to # continue to work for the ensure block of the test temp_conn = Person.connection.dup @@ -134,7 +129,7 @@ class MigrationTest < ActiveRecord::TestCase t.column :foo, :string end ensure - Person.connection.drop_table :testings2 rescue nil + Person.connection.drop_table :testings2, if_exists: true end def connection @@ -431,8 +426,6 @@ class MigrationTest < ActiveRecord::TestCase end def test_create_table_with_binary_column - Person.connection.drop_table :binary_testings rescue nil - assert_nothing_raised { Person.connection.create_table :binary_testings do |t| t.column "data", :binary, :null => false @@ -444,7 +437,7 @@ class MigrationTest < ActiveRecord::TestCase assert_nil data_column.default - Person.connection.drop_table :binary_testings rescue nil + Person.connection.drop_table :binary_testings, if_exists: true end unless mysql_enforcing_gtid_consistency? diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index ad09340518..6d91f96bf6 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -46,28 +46,28 @@ module ActiveRecord def test_quoted_time_utc with_timezone_config default: :utc do - t = Time.now + t = Time.now.change(usec: 0) assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_time_local with_timezone_config default: :local do - t = Time.now + t = Time.now.change(usec: 0) assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_time_crazy with_timezone_config default: :asdfasdf do - t = Time.now + t = Time.now.change(usec: 0) assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_datetime_utc with_timezone_config default: :utc do - t = DateTime.now + t = Time.now.change(usec: 0).to_datetime assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) end end @@ -76,7 +76,7 @@ module ActiveRecord # DateTime doesn't define getlocal, so make sure it does nothing def test_quoted_datetime_local with_timezone_config default: :local do - t = DateTime.now + t = Time.now.change(usec: 0).to_datetime assert_equal t.to_s(:db), @quoter.quoted_date(t) end end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 02b32abebf..4bfffbe9c6 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -208,6 +208,12 @@ class RelationScopingTest < ActiveRecord::TestCase assert_equal [], DeveloperFilteredOnJoins.all assert_not_equal [], Developer.all end + + def test_current_scope_does_not_pollute_other_subclasses + Post.none.scoping do + assert StiPost.all.any? + end + end end class NestedRelationScopingTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb new file mode 100644 index 0000000000..23422fd50a --- /dev/null +++ b/activerecord/test/cases/time_precision_test.rb @@ -0,0 +1,65 @@ +require 'cases/helper' + +if ActiveRecord::Base.connection.supports_datetime_with_precision? +class TimePrecisionTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + teardown do + @connection.drop_table :foos, if_exists: true + end + + def test_time_data_type_with_precision + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :start, :time, precision: 3 + @connection.add_column :foos, :finish, :time, precision: 6 + assert_equal 3, activerecord_column_option('foos', 'start', 'precision') + assert_equal 6, activerecord_column_option('foos', 'finish', 'precision') + end + + def test_passing_precision_to_time_does_not_set_limit + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 3 + t.time :finish, precision: 6 + end + assert_nil activerecord_column_option('foos', 'start', 'limit') + assert_nil activerecord_column_option('foos', 'finish', 'limit') + end + + def test_invalid_time_precision_raises_error + assert_raises ActiveRecord::ActiveRecordError do + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 7 + t.time :finish, precision: 7 + end + end + end + + def test_database_agrees_with_activerecord_about_precision + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 2 + t.time :finish, precision: 4 + end + assert_equal 2, database_datetime_precision('foos', 'start') + assert_equal 4, database_datetime_precision('foos', 'finish') + end + + private + + def database_datetime_precision(table_name, column_name) + results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") + result = results.find do |result_hash| + result_hash["column_name"] == column_name + end + result && result["datetime_precision"].to_i + end + + def activerecord_column_option(tablename, column_name, option) + result = @connection.columns(tablename).find do |column| + column.name == column_name + end + result && result.send(option) + end +end +end diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index d35d34ff2d..34b6f2e8a5 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -108,16 +108,6 @@ module ActiveRecord assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) end - if current_adapter?(:SQLite3Adapter) - def test_binary_encoding - type = SQLite3Binary.new - utf8_string = "a string".encode(Encoding::UTF_8) - type_cast = type.type_cast_from_user(utf8_string) - - assert_equal Encoding::ASCII_8BIT, type_cast.encoding - end - end - def test_attributes_which_are_invalid_for_database_can_still_be_reassigned type_which_cannot_go_to_the_database = Type::Value.new def type_which_cannot_go_to_the_database.type_cast_for_database(*) |