diff options
Diffstat (limited to 'activerecord')
62 files changed, 744 insertions, 157 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index be3a461eda..b6a943eca7 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,11 +1,79 @@ -* Add `config.active_record.warn_on_records_fetched_greater_than` option +* Make `unscope` aware of "less than" and "greater than" conditions. + + *TAKAHASHI Kazuaki* + +* `find_by` and `find_by!` raise `ArgumentError` when called without + arguments. + + *Kohei Suzuki* + +* Revert behavior of `db:schema:load` back to loading the full + environment. This ensures that initializers are run. + + Fixes #19545. + + *Yves Senn* + +* Fix missing index when using `timestamps` with the `index` option. + + The `index` option used with `timestamps` should be passed to both + `column` definitions for `created_at` and `updated_at` rather than just + the first. + + *Paul Mucur* + +* Rename `:class` to `:anonymous_class` in association options. + + Fixes #19659. + + *Andrew White* + +* Autosave existing records on a has many through association when the parent + is new. + + Fixes #19782. + + *Sean Griffin* + +* Fixed a bug where uniqueness validations would error on out of range values, + even if an validation should have prevented it from hitting the database. + + *Andrey Voronkov* + +* MySQL: `:charset` and `:collation` support for string and text columns. + + Example: + + create_table :foos do |t| + t.string :string_utf8_bin, charset: 'utf8', collation: 'utf8_bin' + t.text :text_ascii, charset: 'ascii' + end + + *Ryuta Kamizono* + +* Foreign key related methods in the migration DSL respect + `ActiveRecord::Base.pluralize_table_names = false`. + + Fixes #19643. + + *Mehmet Emin İNAÇ* + +* Reduce memory usage from loading types on PostgreSQL. + + Fixes #19578. + + *Sean Griffin* + +* Add `config.active_record.warn_on_records_fetched_greater_than` option. When set to an integer, a warning will be logged whenever a result set - larger than the specified size is returned by a query. Fixes #16463 + larger than the specified size is returned by a query. + + Fixes #16463. *Jason Nochlin* -* Ignore psqlrc when loading database structure. +* Ignore `.psqlrc` when loading database structure. *Jason Weathered* diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index c88b9e8718..bd95b57303 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Object-relational mapper framework (part of Rails).' s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.' - s.required_ruby_version = '>= 2.2.1' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 58a4694880..1844b29ccb 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -49,6 +49,7 @@ module ActiveRecord autoload :ModelSchema autoload :NestedAttributes autoload :NoTouching + autoload :TouchLater autoload :Persistence autoload :QueryCache autoload :Querying diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 5e3e5f709b..c5c2178ee2 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1565,10 +1565,7 @@ module ActiveRecord # # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration # def change - # create_table :developers_projects, id: false do |t| - # t.integer :developer_id - # t.integer :project_id - # end + # create_join_table :developers, :projects # end # end # @@ -1718,10 +1715,8 @@ module ActiveRecord join_model = builder.through_model - # FIXME: we should move this to the internal constants. Also people - # should never directly access this constant so I'm not happy about - # setting it. const_set join_model.name, join_model + private_constant join_model.name middle_reflection = builder.middle_reflection join_model diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 88406740d8..ba1b1814d1 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -16,7 +16,7 @@ module ActiveRecord::Associations::Builder end self.extensions = [] - VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] # :nodoc: + VALID_OPTIONS = [:class_name, :anonymous_class, :foreign_key, :validate] # :nodoc: def self.build(model, name, scope, options, &block) if model.dangerous_attribute_method?(name) diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index ec135d49b7..97eb007f62 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -60,7 +60,7 @@ module ActiveRecord::Associations::Builder klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) end - def self.touch_record(o, foreign_key, name, touch) # :nodoc: + def self.touch_record(o, foreign_key, name, touch, touch_method) # :nodoc: old_foreign_id = o.changed_attributes[foreign_key] if old_foreign_id @@ -75,9 +75,9 @@ module ActiveRecord::Associations::Builder if old_record if touch != true - old_record.touch touch + old_record.send(touch_method, touch) else - old_record.touch + old_record.send(touch_method) end end end @@ -85,9 +85,9 @@ module ActiveRecord::Associations::Builder record = o.send name if record && record.persisted? if touch != true - record.touch touch + record.send(touch_method, touch) else - record.touch + record.send(touch_method) end end end @@ -98,7 +98,8 @@ module ActiveRecord::Associations::Builder touch = reflection.options[:touch] callback = lambda { |record| - BelongsTo.touch_record(record, foreign_key, n, touch) + touch_method = touching_delayed_records? ? :touch : :touch_later + BelongsTo.touch_record(record, foreign_key, n, touch, touch_method) } model.after_save callback, if: :changed? diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 93dc4ae118..ffd9c9d6fc 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -78,14 +78,14 @@ module ActiveRecord::Associations::Builder join_model.table_name_resolver = habtm join_model.class_resolver = lhs_model - join_model.add_left_association :left_side, class: lhs_model + join_model.add_left_association :left_side, anonymous_class: lhs_model join_model.add_right_association association_name, belongs_to_options(options) join_model end def middle_reflection(join_model) middle_name = [lhs_model.name.downcase.pluralize, - association_name].join('_').gsub(/::/, '_').to_sym + association_name].join('_'.freeze).gsub('::'.freeze, '_'.freeze).to_sym middle_options = middle_options join_model HasMany.create_reflection(lhs_model, diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 88531205a1..6caadb4ce8 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -370,6 +370,8 @@ module ActiveRecord replace_common_records_in_memory(other_array, original_target) if other_array != original_target transaction { replace_records(other_array, original_target) } + else + other_array end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 4897ec44e9..cd79266952 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -38,12 +38,10 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) ensure_not_nested - if record.new_record? - if raise - record.save!(:validate => validate) - else - return unless record.save(:validate => validate) - end + if raise + record.save!(:validate => validate) + else + return unless record.save(:validate => validate) end save_through_record(record) @@ -135,7 +133,7 @@ module ActiveRecord if scope.klass.primary_key count = scope.destroy_all.length else - scope.each(&:_run_destroy_callbacks) + scope.each { |record| record.run_callbacks :destroy } arel = scope.arel diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 93550a69f1..9c5b7d937d 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -120,23 +120,22 @@ module ActiveRecord #:nodoc: # All column values are automatically available through basic accessors on the Active Record # object, but sometimes you want to specialize this behavior. This can be done by overwriting # the default accessors (using the same name as the attribute) and calling - # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually - # change things. + # +super+ to actually change things. # # class Song < ActiveRecord::Base # # Uses an integer of seconds to hold the length of the song # # def length=(minutes) - # write_attribute(:length, minutes.to_i * 60) + # super(minutes.to_i * 60) # end # # def length - # read_attribute(:length) / 60 + # super / 60 # end # end # # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> - # instead of <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>. + # or <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>. # # == Attribute query methods # @@ -309,6 +308,7 @@ module ActiveRecord #:nodoc: include Aggregations include Transactions include NoTouching + include TouchLater include Reflection include Serialization include Store diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index f44e5af5de..2fcba8e309 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -289,25 +289,24 @@ module ActiveRecord end def destroy #:nodoc: - _run_destroy_callbacks { super } + run_callbacks(:destroy) { super } end def touch(*) #:nodoc: - _run_touch_callbacks { super } + run_callbacks(:touch) { super } end private - def create_or_update(*) #:nodoc: - _run_save_callbacks { super } + run_callbacks(:save) { super } end def _create_record #:nodoc: - _run_create_callbacks { super } + run_callbacks(:create) { super } end def _update_record(*) #:nodoc: - _run_update_callbacks { super } + run_callbacks(:update) { super } end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index d99dc9a5db..8c50f3d1a3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -358,7 +358,7 @@ module ActiveRecord synchronize do owner = conn.owner - conn._run_checkin_callbacks do + conn.run_callbacks :checkin do conn.expire end @@ -449,7 +449,7 @@ module ActiveRecord end def checkout_and_verify(c) - c._run_checkout_callbacks do + c.run_callbacks :checkout do c.verify! end c diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index d2840b9498..91c7298983 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -77,7 +77,7 @@ module ActiveRecord # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) - s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode) + s.gsub('\\'.freeze, '\&\&'.freeze).gsub("'".freeze, "''".freeze) # ' (for ruby-mode) end # Quotes the column name. Defaults to no quoting. 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 a768ee2d70..4761024ad0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -138,7 +138,7 @@ module ActiveRecord end def foreign_table_name - name.to_s.pluralize + Base.pluralize_table_names ? name.to_s.pluralize : name end end @@ -359,6 +359,7 @@ module ActiveRecord def column(name, type, options = {}) name = name.to_s type = type.to_sym + options = options.dup if @columns_hash[name] && @columns_hash[name].primary_key? raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." 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 72e019066e..879a47f021 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -120,6 +120,8 @@ module ActiveRecord # [<tt>:id</tt>] # Whether to automatically add a primary key column. Defaults to true. # Join tables for +has_and_belongs_to_many+ should set it to false. + # + # A Symbol can be used to specify the type of the generated primary key column. # [<tt>:primary_key</tt>] # The name of the primary key, if one is to be added automatically. # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. @@ -163,6 +165,19 @@ module ActiveRecord # name varchar(80) # ) # + # ====== Change the primary key column type + # + # create_table(:tags, id: :string) do |t| + # t.column :label, :string + # end + # + # generates: + # + # CREATE TABLE tags ( + # id varchar PRIMARY KEY, + # label varchar + # ) + # # ====== Do not add a primary key column # # create_table(:categories_suppliers, id: false) do |t| @@ -377,6 +392,9 @@ module ActiveRecord # [<tt>:force</tt>] # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. + # [<tt>:if_exists</tt>] + # Set to +true+ to only drop the table if it exists. + # Defaults to false. # # Although this command ignores most +options+ and the block if one is given, # it can be helpful to provide these in a migration's +change+ method so it can be reverted. @@ -387,6 +405,10 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. + # + # Note: Not all options will be available, generally this command should + # ignore most of them. In favor of doing a low-level call to simply + # create a column. def add_column(table_name, column_name, type, options = {}) at = create_alter_table table_name at.add_column(column_name, type, options) @@ -530,6 +552,8 @@ module ActiveRecord # # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active # + # Note: Partial indexes are only supported for PostgreSQL and SQLite 3.8.0+. + # # ====== Creating an index with a specific method # # add_index(:developers, :name, using: 'btree') @@ -667,7 +691,10 @@ module ActiveRecord # remove_reference(:products, :user, index: true, foreign_key: true) # def remove_reference(table_name, ref_name, options = {}) - remove_foreign_key table_name, ref_name.to_s.pluralize if options[:foreign_key] + if options[:foreign_key] + reference_name = Base.pluralize_table_names ? ref_name.to_s.pluralize : ref_name + remove_foreign_key(table_name, reference_name) + end remove_column(table_name, "#{ref_name}_id") remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] @@ -684,8 +711,8 @@ module ActiveRecord # +to_table+ contains the referenced primary key. # # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>. - # +identifier+ is a 10 character long random string. A custom name can be specified with - # the <tt>:name</tt> option. + # +identifier+ is a 10 character long string which is deterministically generated from the + # +from_table+ and +column+. A custom name can be specified with the <tt>:name</tt> option. # # ====== Creating a simple foreign key # 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 b7c7ff1187..76aee452ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -13,6 +13,10 @@ module ActiveRecord end end + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :charset, :collation + end + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods @@ -23,8 +27,16 @@ module ActiveRecord column.type = :integer column.auto_increment = true end + column.charset = options[:charset] + column.collation = options[:collation] column end + + private + + def create_column_definition(name, type) + ColumnDefinition.new(name, type) + end end class Table < ActiveRecord::ConnectionAdapters::Table @@ -60,6 +72,23 @@ module ActiveRecord add_column_position!(change_column_sql, column_options(o.column)) end + def column_options(o) + column_options = super + column_options[:charset] = o.charset + column_options[:collation] = o.collation + column_options + end + + def add_column_options!(sql, options) + if options[:charset] + sql << " CHARACTER SET #{options[:charset]}" + end + if options[:collation] + sql << " COLLATE #{options[:collation]}" + end + super + end + def add_column_position!(sql, options) if options[:first] sql << " FIRST" @@ -99,9 +128,18 @@ module ActiveRecord spec = super spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0 spec.delete(:limit) if :boolean === column.type + if column.collation && table_name = column.instance_variable_get(:@table_name) + @collation_cache ||= {} + @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] + spec[:collation] = column.collation.inspect if column.collation != @collation_cache[table_name] + end spec end + def migration_keys + super + [:collation] + end + class Column < ConnectionAdapters::Column # :nodoc: delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true @@ -562,6 +600,21 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end + # Drops a table from the database. + # + # [<tt>:force</tt>] + # Set to +:cascade+ to drop dependent objects as well. + # Defaults to false. + # [<tt>:if_exists</tt>] + # Set to +true+ to only drop the table if it exists. + # Defaults to false. + # [<tt>:temporary</tt>] + # Set to +true+ to drop temporary table. + # Defaults to false. + # + # Although this command ignores most +options+ and the block if one is given, + # it can be helpful to provide these in a migration's +change+ method so it can be reverted. + # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index a67127bd71..f4dda5154e 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -28,6 +28,7 @@ module ActiveRecord @null = null @default = default @default_function = default_function + @table_name = nil end def has_default? diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 45b935f1d6..18febf66b4 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -5,8 +5,10 @@ require 'active_support/core_ext/hash/keys' gem 'mysql', '~> 2.9' require 'mysql' -class Mysql +class Mysql # :nodoc: all class Time + # Used for casting DateTime fields to a MySQL friendly Time. + # This was documented in 48498da0dfed5239ea1eafb243ce47d7e3ce9e8e def to_date Date.new(year, month, day) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 9d421d6975..191c828e60 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -15,10 +15,10 @@ module ActiveRecord def run(records) nodes = records.reject { |row| @store.key? row['oid'].to_i } mapped, nodes = nodes.partition { |row| @store.key? row['typname'] } - ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' } - enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } - domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } - arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + ranges, nodes = nodes.partition { |row| row['typtype'] == 'r'.freeze } + enums, nodes = nodes.partition { |row| row['typtype'] == 'e'.freeze } + domains, nodes = nodes.partition { |row| row['typtype'] == 'd'.freeze } + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in'.freeze } composites, nodes = nodes.partition { |row| row['typelem'].to_i != 0 } mapped.each { |row| register_mapped_type(row) } @@ -29,6 +29,18 @@ module ActiveRecord composites.each { |row| register_composite_type(row) } end + def query_conditions_for_initial_load(type_map) + known_type_names = type_map.keys.map { |n| "'#{n}'" } + known_type_types = %w('r' 'e' 'd') + <<-SQL % [known_type_names.join(", "), known_type_types.join(", ")] + WHERE + t.typname IN (%s) + OR t.typtype IN (%s) + OR t.typinput::varchar = 'array_in' + OR t.typelem != 0 + SQL + end + private def register_mapped_type(row) alias_type row['oid'], row['typname'] 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 cb0b9cdbe9..168180cfd3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -87,7 +87,7 @@ module ActiveRecord SQL end - def drop_table(table_name, options = {}) + def drop_table(table_name, options = {}) # :nodoc: execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end @@ -422,7 +422,7 @@ module ActiveRecord end # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) + def change_column_default(table_name, column_name, default) # :nodoc: clear_cache! column = column_for(table_name, column_name) return unless column diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e616cee5eb..332ac9d88c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -568,9 +568,9 @@ module ActiveRecord case default # Quoted types when /\A[\(B]?'(.*)'::/m - $1.gsub(/''/, "'") + $1.gsub("''".freeze, "'".freeze) # Boolean types - when 'true', 'false' + when 'true'.freeze, 'false'.freeze default # Numeric types when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ @@ -594,6 +594,8 @@ module ActiveRecord end def load_additional_types(type_map, oids = nil) # :nodoc: + initializer = OID::TypeMapInitializer.new(type_map) + if supports_ranges? query = <<-SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype @@ -609,11 +611,13 @@ module ActiveRecord if oids query += "WHERE t.oid::integer IN (%s)" % oids.join(", ") + else + query += initializer.query_conditions_for_initial_load(type_map) end - initializer = OID::TypeMapInitializer.new(type_map) - records = execute(query, 'SCHEMA') - initializer.run(records) + execute_and_clear(query, 'SCHEMA', []) do |records| + initializer.run(records) + end end FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: @@ -823,9 +827,11 @@ module ActiveRecord 'float8' => PG::TextDecoder::Float, 'bool' => PG::TextDecoder::Boolean, } - query = <<-SQL + known_coder_types = coders_by_name.keys.map { |n| quote(n) } + query = <<-SQL % known_coder_types.join(", ") SELECT t.oid, t.typname FROM pg_type as t + WHERE t.typname IN (%s) SQL coders = execute_and_clear(query, "SCHEMA", []) do |result| result diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index a17eb226f4..1ad910c4bc 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -96,10 +96,10 @@ module ActiveRecord ## # :singleton-method: - # Specify a threshold for the size of query result sets. If the - # number of records in the set exceeds threshold, a warning is - # logged. This should be used to identify queries which pull - # thousands of records, which may cause memory bloat. + # Specify a threshold for the size of query result sets. If the number of + # records in the set exceeds the threshold, a warning is logged. This can + # be used to identify queries which load thousands of records and + # potentially cause memory bloat. mattr_accessor :warn_on_records_fetched_greater_than, instance_writer: false self.warn_on_records_fetched_greater_than = nil @@ -302,18 +302,22 @@ module ActiveRecord assign_attributes(attributes) if attributes yield self if block_given? - _run_initialize_callbacks + run_callbacks :initialize end - # Initialize an empty model object from +coder+. +coder+ must contain - # the attributes necessary for initializing an empty model object. For - # example: + # Initialize an empty model object from +coder+. +coder+ should be + # the result of previously encoding an Active Record model, using + # `encode_with` # # class Post < ActiveRecord::Base # end # + # old_post = Post.new(title: "hello world") + # coder = {} + # old_post.encode_with(coder) + # # post = Post.allocate - # post.init_with('attributes' => { 'title' => 'hello world' }) + # post.init_with(coder) # post.title # => 'hello world' def init_with(coder) coder = LegacyYamlAdapter.convert(self.class, coder) @@ -325,8 +329,8 @@ module ActiveRecord self.class.define_attribute_methods - _run_find_callbacks - _run_initialize_callbacks + run_callbacks :find + run_callbacks :initialize self end @@ -362,7 +366,7 @@ module ActiveRecord @attributes = @attributes.dup @attributes.reset(self.class.primary_key) - _run_initialize_callbacks + run_callbacks(:initialize) @new_record = true @destroyed = false diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index ea88983917..2b99899e42 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -32,7 +32,7 @@ module ActiveRecord # Conversation.active # Conversation.archived # - # Of course, you can also query them directly if the scopes doesn't fit your + # Of course, you can also query them directly if the scopes don't fit your # needs: # # Conversation.where(status: [:active, :archived]) diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 6b26d7be78..af816a278e 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -31,9 +31,10 @@ module ActiveRecord end def sql(event) - self.class.runtime += event.duration return unless logger.debug? + self.class.runtime += event.duration + payload = event.payload return if IGNORE_PAYLOAD_NAMES.include?(payload[:name]) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 75adcccce6..3674f672cb 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -328,12 +328,8 @@ module ActiveRecord @default_attributes = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @attributes_builder = nil - @column_names = nil - @attribute_types = nil @columns = nil @columns_hash = nil - @content_columns = nil - @default_attributes = nil @attribute_names = nil end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a6176dffdb..da8f4d027a 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -205,7 +205,9 @@ module ActiveRecord # instance using the companies/company partial instead of clients/client. # # Note: The new instance will share a link to the same attributes as the original class. - # So any change to the attributes in either instance will affect the other. + # Therefore the sti column value will still be the same. + # Any change to the attributes on either instance will affect both instances. + # If you want to change the sti column as well, use +becomes!+ instead. def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) @@ -428,7 +430,7 @@ module ActiveRecord self end - # Saves the record with the updated_at/on attributes set to the current time + # Saves the record with the updated_at/on attributes set to the current time # or the time specified. # Please note that no validation is performed and only the +after_touch+, # +after_commit+ and +after_rollback+ callbacks are executed. @@ -462,9 +464,10 @@ module ActiveRecord # ball = Ball.new # ball.touch(:updated_at) # => raises ActiveRecordError # - def touch(*names, time: current_time_from_proper_timezone) + def touch(*names, time: nil) raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + time ||= current_time_from_proper_timezone attributes = timestamp_attributes_for_update_in_model attributes.concat(names) @@ -476,11 +479,23 @@ module ActiveRecord changes[column] = write_attribute(column, time) end - changes[self.class.locking_column] = increment_lock if locking_enabled? - clear_attribute_changes(changes.keys) primary_key = self.class.primary_key - self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 + scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key)) + + if locking_enabled? + locking_column = self.class.locking_column + scope = scope.where(locking_column => _read_attribute(locking_column)) + changes[locking_column] = increment_lock + end + + result = scope.update_all(changes) == 1 + + if !result && locking_enabled? + raise ActiveRecord::StaleObjectError.new(self, "touch") + end + + result else true end diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index af4840476c..8727e46cb3 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -19,7 +19,7 @@ module ActiveRecord end def cleanup_view_runtime - if ActiveRecord::Base.connected? + if logger.info? && ActiveRecord::Base.connected? db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime self.db_runtime = (db_runtime || 0) + db_rt_before_render runtime = super diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 2591e7492d..d168786e71 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -240,7 +240,7 @@ db_namespace = namespace :db do end desc 'Load a schema.rb file into the database' - task :load => [:load_config] do + task :load => [:environment, :load_config] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 4265afc0a5..1b0ae2c942 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -196,7 +196,7 @@ module ActiveRecord @scope = scope @options = options @active_record = active_record - @klass = options[:class] + @klass = options[:anonymous_class] @plural_name = active_record.pluralize_table_names ? name.to_s.pluralize : name.to_s end @@ -635,7 +635,7 @@ module ActiveRecord def initialize(delegate_reflection) @delegate_reflection = delegate_reflection - @klass = delegate_reflection.options[:class] + @klass = delegate_reflection.options[:anonymous_class] @source_reflection_name = delegate_reflection.options[:source] end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 8f16de3519..402b317d9c 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -130,9 +130,9 @@ module ActiveRecord # the plucked column names, if they can be deduced. Plucking an SQL fragment # returns String values by default. # - # Person.pluck(:id) - # # SELECT people.id FROM people - # # => [1, 2, 3] + # Person.pluck(:name) + # # SELECT people.name FROM people + # # => ['David', 'Jeremy', 'Jose'] # # Person.pluck(:id, :name) # # SELECT people.id, people.name FROM people @@ -150,6 +150,8 @@ module ActiveRecord # # SELECT DATEDIFF(updated_at, created_at) FROM people # # => ['0', '27761', '173'] # + # See also +ids+. + # def pluck(*column_names) column_names.map! do |column_name| if column_name.is_a?(Symbol) && attribute_alias?(column_name) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3f747a20b3..576a32bf75 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -5,7 +5,7 @@ module ActiveRecord ONE_AS_ONE = '1 AS one' # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). - # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key + # If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key # is an integer, find by id coerces its arguments using +to_i+. # # Person.find(1) # returns the object for ID = 1 @@ -16,8 +16,6 @@ module ActiveRecord # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # - # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found. - # # NOTE: The returned records may not be in the same order as the ids you # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt> # option if you want the results are sorted. @@ -79,16 +77,16 @@ module ActiveRecord # # Post.find_by name: 'Spartacus', rating: 4 # Post.find_by "published_at < ?", 2.weeks.ago - def find_by(*args) - where(*args).take + def find_by(arg, *args) + where(arg, *args).take rescue RangeError nil end # Like <tt>find_by</tt>, except that if no record is found, raises # an <tt>ActiveRecord::RecordNotFound</tt> error. - def find_by!(*args) - where(*args).take! + def find_by!(arg, *args) + where(arg, *args).take! rescue RangeError raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value" end diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb index 0d31f73ddd..14e1bf89fa 100644 --- a/activerecord/lib/active_record/relation/record_fetch_warning.rb +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -5,9 +5,8 @@ module ActiveRecord # `config.active_record.warn_on_records_fetched_greater_than` is # set to an integer, if the number of records a query returns is # greater than the value of `warn_on_records_fetched_greater_than`, - # a warning is logged. This allows for the dection of queries that - # return a large number of records, which could cause memory - # bloat. + # a warning is logged. This allows for the detection of queries that + # return a large number of records, which could cause memory bloat. # # In most cases, fetching large number of records can be performed # efficiently using the ActiveRecord::Batches methods. diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index da95920571..eaeaf0321b 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -105,7 +105,10 @@ HEADER end def table(table, stream) - columns = @connection.columns(table) + columns = @connection.columns(table).map do |column| + column.instance_variable_set(:@table_name, table) + column + end begin tbl = StringIO.new diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 5ec2c88b47..3590b8846e 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -35,7 +35,7 @@ module ActiveRecord # Are there attributes associated with this scope? def scope_attributes? # :nodoc: - super || default_scopes.any? + super || default_scopes.any? || respond_to?(:default_scope) end def before_remove_const #:nodoc: diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb new file mode 100644 index 0000000000..4352a0ffea --- /dev/null +++ b/activerecord/lib/active_record/touch_later.rb @@ -0,0 +1,50 @@ +module ActiveRecord + # = Active Record Touch Later + module TouchLater + extend ActiveSupport::Concern + + included do + before_commit_without_transaction_enrollment :touch_deferred_attributes + end + + def touch_later(*names) # :nodoc: + raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + + @_defer_touch_attrs ||= timestamp_attributes_for_update_in_model + @_defer_touch_attrs |= names + @_touch_time = current_time_from_proper_timezone + + surreptitiously_touch @_defer_touch_attrs + self.class.connection.add_transaction_record self + end + + def touch(*names, time: nil) # :nodoc: + if has_defer_touch_attrs? + names |= @_defer_touch_attrs + end + super(*names, time: time) + end + + private + def surreptitiously_touch(attrs) + attrs.each { |attr| write_attribute attr, @_touch_time } + clear_attribute_changes attrs + end + + def touch_deferred_attributes + if has_defer_touch_attrs? && persisted? + @_touching_delayed_records = true + touch(*@_defer_touch_attrs, time: @_touch_time) + @_touching_delayed_records, @_defer_touch_attrs, @_touch_time = nil, nil, nil + end + end + + def has_defer_touch_attrs? + defined?(@_defer_touch_attrs) && @_defer_touch_attrs.present? + end + + def touching_delayed_records? + defined?(@_touching_delayed_records) && @_touching_delayed_records + end + end +end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 2293d1b258..311dacb449 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -319,8 +319,8 @@ module ActiveRecord end def before_committed! # :nodoc: - _run_before_commit_without_transaction_enrollment_callbacks - _run_before_commit_callbacks + run_callbacks :before_commit_without_transaction_enrollment + run_callbacks :before_commit end # Call the +after_commit+ callbacks. @@ -329,8 +329,8 @@ module ActiveRecord # but call it after the commit of a destroyed object. def committed!(should_run_callbacks: true) #:nodoc: if should_run_callbacks && destroyed? || persisted? - _run_commit_without_transaction_enrollment_callbacks - _run_commit_callbacks + run_callbacks :commit_without_transaction_enrollment + run_callbacks :commit end ensure force_clear_transaction_record_state @@ -340,8 +340,8 @@ module ActiveRecord # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: if should_run_callbacks - _run_rollback_without_transaction_enrollment_callbacks - _run_rollback_callbacks + run_callbacks :rollback + run_callbacks :rollback_without_transaction_enrollment end ensure restore_transaction_record_state(force_restore_state) diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb index 82d9327fc0..3b01e3f8ca 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -1,12 +1,18 @@ module ActiveRecord module Type class HashLookupTypeMap < TypeMap # :nodoc: - delegate :key?, to: :@mapping - def alias_type(type, alias_type) register_type(type) { |_, *args| lookup(alias_type, *args) } end + def key?(key) + @mapping.key?(key) + end + + def keys + @mapping.keys + end + private def perform_fetch(type, *args, &block) diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 75d5bd5a35..a9b791397b 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -43,6 +43,10 @@ module ActiveRecord # deletes the associated object, thus putting the parent object into an invalid # state. # + # NOTE: This validation will not fail while using it with an association + # if the latter was assigned but not valid. If you want to ensure that + # it is both present and valid, you also need to use +validates_associated+. + # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <tt>:on</tt> - Specifies the contexts where this validation is active. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 9be4b10a55..5106f4e127 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -76,6 +76,8 @@ module ActiveRecord klass.connection.case_sensitive_comparison(table, attribute, column, value) end klass.unscoped.where(comparison) + rescue RangeError + klass.none end def scope_relation(record, table, relation) diff --git a/activerecord/test/cases/adapters/mysql/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb new file mode 100644 index 0000000000..c8dd49d00a --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class CharsetCollationTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :charset_collations, force: true do |t| + t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' + t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' + end + end + + teardown do + @connection.drop_table :charset_collations, if_exists: true + end + + test "string column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + assert_equal :string, column.type + assert_equal 'ascii_bin', column.collation + end + + test "text column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } + assert_equal :text, column.type + assert_equal 'ucs2_unicode_ci', column.collation + end + + test "add column with charset and collation" do + @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'utf8_bin', column.collation + end + + test "change column with charset and collation" do + @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' + @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'utf8_general_ci', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("charset_collations") + assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + end +end diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb new file mode 100644 index 0000000000..c8dd49d00a --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class CharsetCollationTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :charset_collations, force: true do |t| + t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' + t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' + end + end + + teardown do + @connection.drop_table :charset_collations, if_exists: true + end + + test "string column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + assert_equal :string, column.type + assert_equal 'ascii_bin', column.collation + end + + test "text column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } + assert_equal :text, column.type + assert_equal 'ucs2_unicode_ci', column.collation + end + + test "add column with charset and collation" do + @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'utf8_bin', column.collation + end + + test "change column with charset and collation" do + @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' + @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'utf8_general_ci', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("charset_collations") + assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + end +end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 47fd7345c8..ba90c61d65 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -124,9 +124,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase where("id = :inc", :inc => counter) } - has_many :comments, :class => comments + has_many :comments, :anonymous_class => comments } - belongs_to :post, :class => posts, :inverse_of => false + belongs_to :post, :anonymous_class => posts, :inverse_of => false } assert_equal 0, counter @@ -422,6 +422,24 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(1) { line_item.touch } end + def test_belongs_to_with_touch_on_multiple_records + line_item = LineItem.create!(amount: 1) + line_item2 = LineItem.create!(amount: 2) + Invoice.create!(line_items: [line_item, line_item2]) + + assert_queries(1) do + LineItem.transaction do + line_item.touch + line_item2.touch + end + end + + assert_queries(2) do + line_item.touch + line_item2.touch + end + end + def test_belongs_to_with_touch_option_on_touch_without_updated_at_attributes assert_not LineItem.column_names.include?("updated_at") diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 7d8b933992..0ecf2ddfd1 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -759,6 +759,23 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_eager_with_default_scope_as_class_method_using_find_method + david = developers(:david) + developer = EagerDeveloperWithClassMethodDefaultScope.find(david.id) + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + + def test_eager_with_default_scope_as_class_method_using_find_by_method + developer = EagerDeveloperWithClassMethodDefaultScope.find_by(name: 'David') + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + def test_eager_with_default_scope_as_lambda developer = EagerDeveloperWithLambdaDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 290b2a0d6b..14cdf37f46 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -119,9 +119,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase developer_project = Class.new(ActiveRecord::Base) { self.table_name = 'developers_projects' - belongs_to :developer, :class => dev + belongs_to :developer, :anonymous_class => dev } - has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id' + has_many :developer_projects, :anonymous_class => developer_project, :foreign_key => 'developer_id' } dev = developer.first named = Developer.find(dev.id) @@ -140,13 +140,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase comments = Class.new(ActiveRecord::Base) { self.table_name = 'comments' self.inheritance_column = 'not_there' - belongs_to :post, :class => post + belongs_to :post, :anonymous_class => post default_scope -> { counter += 1 where("id = :inc", :inc => counter) } } - has_many :comments, :class => comments, :foreign_key => 'post_id' + has_many :comments, :anonymous_class => comments, :foreign_key => 'post_id' } assert_equal 0, counter post = posts.first @@ -1503,6 +1503,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(0, ignore_none: true) do firm.clients = [] end + + assert_equal [], firm.send('clients=', []) end def test_transactions_when_replacing_on_persisted diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 5f52c65412..9734ea2217 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -84,11 +84,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase subscriber = make_model "Subscriber" subscriber.primary_key = 'nick' - subscription.belongs_to :book, class: book - subscription.belongs_to :subscriber, class: subscriber + subscription.belongs_to :book, anonymous_class: book + subscription.belongs_to :subscriber, anonymous_class: subscriber - book.has_many :subscriptions, class: subscription - book.has_many :subscribers, through: :subscriptions, class: subscriber + book.has_many :subscriptions, anonymous_class: subscription + book.has_many :subscribers, through: :subscriptions, anonymous_class: subscriber anonbook = book.first namebook = Book.find anonbook.id @@ -154,10 +154,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase lesson_student = make_model 'LessonStudent' lesson_student.table_name = 'lessons_students' - lesson_student.belongs_to :lesson, :class => lesson - lesson_student.belongs_to :student, :class => student - lesson.has_many :lesson_students, :class => lesson_student - lesson.has_many :students, :through => :lesson_students, :class => student + lesson_student.belongs_to :lesson, :anonymous_class => lesson + lesson_student.belongs_to :student, :anonymous_class => student + lesson.has_many :lesson_students, :anonymous_class => lesson_student + lesson.has_many :students, :through => :lesson_students, :anonymous_class => student [lesson, lesson_student, student] end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 8f0d7bd037..80b5a0004d 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -43,7 +43,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase reference = Class.new(ActiveRecord::Base) { self.table_name = "references" def self.name; 'Reference'; end - belongs_to :person, autosave: true, class: person + belongs_to :person, autosave: true, anonymous_class: person } u = person.create!(first_name: 'cool') @@ -1278,6 +1278,16 @@ module AutosaveAssociationOnACollectionAssociationTests assert_equal new_names, @pirate.reload.send(@association_name).map(&:name) end + def test_should_update_children_when_autosave_is_true_and_parent_is_new_but_child_is_not + parrot = Parrot.create!(name: "Polly") + parrot.name = "Squawky" + pirate = Pirate.new(parrots: [parrot], catchphrase: "Arrrr") + + pirate.save! + + assert_equal "Squawky", parrot.reload.name + end + def test_should_automatically_validate_the_associated_models @pirate.send(@association_name).each { |child| child.name = '' } diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 9e428098e4..0791dde1f2 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -190,8 +190,9 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified assert_queries(Subscriber.count + 1) do - Subscriber.find_each(:batch_size => 1) do |subscriber| - assert_kind_of Subscriber, subscriber + Subscriber.find_in_batches(batch_size: 1) do |batch| + assert_kind_of Array, batch + assert_kind_of Subscriber, batch.first end end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index f53c496ecd..bab624b78a 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -110,7 +110,7 @@ module ActiveRecord assert ActiveRecord::Base.connection_handler.active_connections? end - test "proxy is polite to it's body and responds to it" do + test "proxy is polite to its body and responds to it" do body = Class.new(String) { def to_path; "/path"; end }.new app = lambda { |_| [200, {}, body] } response_body = ConnectionManagement.new(app).call(@env)[2] diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 8d15a76735..aa50efc979 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -204,13 +204,13 @@ module ActiveRecord end # The connection pool is "fair" if threads waiting for - # connections receive them the order in which they began + # connections receive them in the order in which they began # waiting. This ensures that we don't timeout one HTTP request # even while well under capacity in a multi-threaded environment # such as a Java servlet container. # # We don't need strict fairness: if two connections become - # available at the same time, it's fine of two threads that were + # available at the same time, it's fine if two threads that were # waiting acquire the connections out of order. # # Thus this test prepares waiting threads and then trickles in diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb index 55f0e51717..c25089a420 100644 --- a/activerecord/test/cases/disconnected_test.rb +++ b/activerecord/test/cases/disconnected_test.rb @@ -21,7 +21,9 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase @connection.execute "SELECT count(*) from products" @connection.disconnect! assert_raises(ActiveRecord::StatementInvalid) do - @connection.execute "SELECT count(*) from products" + silence_warnings do + @connection.execute "SELECT count(*) from products" + end end end end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index f8acdcb51e..97ba178b4d 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -279,10 +279,10 @@ class HasManyThroughFixture < ActiveSupport::TestCase treasure = make_model "Treasure" pt.table_name = "parrots_treasures" - pt.belongs_to :parrot, :class => parrot - pt.belongs_to :treasure, :class => treasure + pt.belongs_to :parrot, :anonymous_class => parrot + pt.belongs_to :treasure, :anonymous_class => treasure - parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :parrot_treasures, :anonymous_class => pt parrot.has_many :treasures, :through => :parrot_treasures parrots = File.join FIXTURES_ROOT, 'parrots' @@ -297,10 +297,10 @@ class HasManyThroughFixture < ActiveSupport::TestCase parrot = make_model "Parrot" treasure = make_model "Treasure" - pt.belongs_to :parrot, :class => parrot - pt.belongs_to :treasure, :class => treasure + pt.belongs_to :parrot, :anonymous_class => parrot + pt.belongs_to :treasure, :anonymous_class => treasure - parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :parrot_treasures, :anonymous_class => pt parrot.has_many :treasures, :through => :parrot_treasures parrots = File.join FIXTURES_ROOT, 'parrots' diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 9e4998a946..dbdcc84b7d 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -177,6 +177,16 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 1, p1.lock_version end + def test_touch_stale_object + person = Person.create!(first_name: 'Mehmet Emin') + stale_person = Person.find(person.id) + person.update_attribute(:gender, 'M') + + assert_raises(ActiveRecord::StaleObjectError) do + stale_person.touch + end + end + def test_lock_column_name_existing t1 = LegacyThing.find(1) t2 = LegacyThing.find(1) diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 17ac72a109..1594f99852 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -105,7 +105,49 @@ module ActiveRecord @connection.remove_reference :testings, :testing_parent, foreign_key: true end end + + test "foreign key methods respect pluralize_table_names" do + begin + original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names + ActiveRecord::Base.pluralize_table_names = false + @connection.create_table :testing + @connection.change_table :testing_parents do |t| + t.references :testing, foreign_key: true + end + + fk = @connection.foreign_keys("testing_parents").first + assert_equal "testing_parents", fk.from_table + assert_equal "testing", fk.to_table + + assert_difference "@connection.foreign_keys('testing_parents').size", -1 do + @connection.remove_reference :testing_parents, :testing, foreign_key: true + end + ensure + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names + @connection.drop_table "testing", if_exists: true + end + end end end end +else +class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table("testings", if_exists: true) + @connection.drop_table("testing_parents", if_exists: true) + end + + test "ignores foreign keys defined with the table" do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + assert_includes @connection.tables, "testings" + end +end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 2370077eb0..1e93e2a05c 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -924,7 +924,8 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal instance.created_at, created_at assert_equal instance.updated_at, updated_at ensure - ActiveRecord::Base.connection.drop_table :widgets + ActiveRecord::Base.connection.drop_table widget.table_name + widget.reset_column_information end end end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 3664a2af70..83be9a75d8 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -260,7 +260,8 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) end teardown do - @connection.drop_table 'widgets', if_exists: true + @connection.drop_table :widgets, if_exists: true + Widget.reset_column_information end test "primary key column type with bigserial" do diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 0cf44388fa..b8e2041b6d 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1657,6 +1657,10 @@ class RelationTest < ActiveRecord::TestCase assert_sql(/^((?!ORDER).)*$/) { Post.all.find_by(author_id: 2) } end + test "find_by requires at least one argument" do + assert_raises(ArgumentError) { Post.all.find_by } + end + test "find_by! with hash conditions returns the first matching record" do assert_equal posts(:eager_other), Post.order(:id).find_by!(author_id: 2) end @@ -1679,6 +1683,10 @@ class RelationTest < ActiveRecord::TestCase end end + test "find_by! requires at least one argument" do + assert_raises(ArgumentError) { Post.all.find_by! } + end + test "loaded relations cannot be mutated by multi value methods" do relation = Post.all relation.to_a diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 6c099719c0..63612e33af 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -73,7 +73,7 @@ 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|xml|uuid|point)\s+"/) + if match = column.match(/\bt\.\w+\s+"/) match[0].length end end.compact diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 7c89b4b9e8..5dab32995c 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -446,6 +446,17 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) end + + def test_index_is_created_for_both_timestamps + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps(:foos, null: true, index: true) + end + + indexes = ActiveRecord::Base.connection.indexes('foos') + assert_equal ['created_at', 'updated_at'], indexes.flat_map(&:columns).sort + ensure + ActiveRecord::Base.connection.drop_table(:foos) + end end class TimestampsWithoutTransactionTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb new file mode 100644 index 0000000000..11804ff90b --- /dev/null +++ b/activerecord/test/cases/touch_later_test.rb @@ -0,0 +1,93 @@ +require 'cases/helper' +require 'models/invoice' +require 'models/line_item' +require 'models/topic' + +class TouchLaterTest < ActiveRecord::TestCase + + def test_touch_laster_raise_if_non_persisted + invoice = Invoice.new + Invoice.transaction do + refute invoice.persisted? + assert_raises(ActiveRecord::ActiveRecordError) do + invoice.touch_later + end + end + end + + def test_touch_later_dont_set_dirty_attributes + invoice = Invoice.create! + invoice.touch_later + refute invoice.changed? + end + + def test_touch_later_update_the_attributes + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + assert_not_equal time.to_i, topic.updated_at.to_i + assert_not_equal time.to_i, topic.created_at.to_i + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + assert_not_equal time.to_i, topic.reload.updated_at.to_i + assert_not_equal time.to_i, topic.reload.created_at.to_i + end + + def test_touch_touches_immediately + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + topic.touch + + assert_not_equal time, topic.reload.updated_at + assert_not_equal time, topic.reload.created_at + end + end + + def test_touch_later_an_association_dont_autosave_parent + time = Time.now.utc - 25.days + line_item = LineItem.create!(amount: 1) + invoice = Invoice.create!(line_items: [line_item]) + invoice.touch(time: time) + + Invoice.transaction do + line_item.update(amount: 2) + assert_equal time.to_i, invoice.reload.updated_at.to_i + end + + assert_not_equal time.to_i, invoice.updated_at.to_i + end + + def test_touch_touches_immediately_with_a_custom_time + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time, topic.updated_at + assert_equal time, topic.created_at + + Topic.transaction do + topic.touch_later(:created_at) + time = Time.now.utc - 2.days + topic.touch(time: time) + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + end + + def test_touch_later_dont_hit_the_db + invoice = Invoice.create! + assert_queries(0) do + invoice.touch_later + end + end +end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 062bc733f9..2608c84be2 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -34,7 +34,22 @@ class TopicWithUniqEvent < Topic validates :event, uniqueness: true end +class BigIntTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, uniqueness: true, inclusion: { in: 0..INT_MAX_VALUE } +end + +class BigIntReverseTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, inclusion: { in: 0..INT_MAX_VALUE } + validates :engines_count, uniqueness: true +end + class UniquenessValidationTest < ActiveRecord::TestCase + INT_MAX_VALUE = 2147483647 + fixtures :topics, 'warehouse-things' repair_validations(Topic, Reply) @@ -86,6 +101,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t2.errors[:title] end + def test_validate_uniqueness_when_integer_out_of_range + entry = BigIntTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + + def test_validate_uniqueness_when_integer_out_of_range_show_order_does_not_matter + entry = BigIntReverseTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + def test_validates_uniqueness_with_newline_chars Topic.validates_uniqueness_of(:title, :case_sensitive => false) diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index d6fd0c4ab0..52d3290c84 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -24,6 +24,11 @@ ActiveRecord::Schema.define do add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + create_table :collation_tests, id: false, force: true do |t| + t.string :string_cs_column, limit: 1, collation: 'utf8_bin' + t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' + end + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -35,15 +40,6 @@ BEGIN END SQL - ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci -SQL - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index b5378341b5..90f5a60d7b 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -24,6 +24,11 @@ ActiveRecord::Schema.define do add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + create_table :collation_tests, id: false, force: true do |t| + t.string :string_cs_column, limit: 1, collation: 'utf8_bin' + t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' + end + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -46,15 +51,6 @@ BEGIN END SQL - ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci -SQL - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL |