diff options
Diffstat (limited to 'activerecord/lib')
54 files changed, 568 insertions, 353 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index ef14e065ae..1844b29ccb 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -43,11 +43,13 @@ module ActiveRecord autoload :Explain autoload :Inheritance autoload :Integration + autoload :LegacyYamlAdapter autoload :Migration autoload :Migrator, 'active_record/migration' 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 cecb2dbd0b..c5c2178ee2 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -113,19 +113,19 @@ module ActiveRecord # These classes will be loaded when associations are created. # So there is no need to eager load them. - autoload :Association, 'active_record/associations/association' - autoload :SingularAssociation, 'active_record/associations/singular_association' - autoload :CollectionAssociation, 'active_record/associations/collection_association' - autoload :ForeignAssociation, 'active_record/associations/foreign_association' - autoload :CollectionProxy, 'active_record/associations/collection_proxy' + autoload :Association + autoload :SingularAssociation + autoload :CollectionAssociation + autoload :ForeignAssociation + autoload :CollectionProxy - autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' - autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' - autoload :HasManyAssociation, 'active_record/associations/has_many_association' - autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association' - autoload :HasOneAssociation, 'active_record/associations/has_one_association' - autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association' - autoload :ThroughAssociation, 'active_record/associations/through_association' + autoload :BelongsToAssociation + autoload :BelongsToPolymorphicAssociation + autoload :HasManyAssociation + autoload :HasManyThroughAssociation + autoload :HasOneAssociation + autoload :HasOneThroughAssociation + autoload :ThroughAssociation module Builder #:nodoc: autoload :Association, 'active_record/associations/builder/association' @@ -139,10 +139,10 @@ module ActiveRecord end eager_autoload do - autoload :Preloader, 'active_record/associations/preloader' - autoload :JoinDependency, 'active_record/associations/join_dependency' - autoload :AssociationScope, 'active_record/associations/association_scope' - autoload :AliasTracker, 'active_record/associations/alias_tracker' + autoload :Preloader + autoload :JoinDependency + autoload :AssociationScope + autoload :AliasTracker end # Returns the association instance for the given name, instantiating it if it doesn't already exist @@ -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/association.rb b/activerecord/lib/active_record/associations/association.rb index 0d8e4ba870..930f678ae8 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -8,12 +8,12 @@ module ActiveRecord # # Association # SingularAssociation - # HasOneAssociation + # HasOneAssociation + ForeignAssociation # HasOneThroughAssociation + ThroughAssociation # BelongsToAssociation # BelongsToPolymorphicAssociation # CollectionAssociation - # HasManyAssociation + # HasManyAssociation + ForeignAssociation # HasManyThroughAssociation + ThroughAssociation class Association #:nodoc: attr_reader :owner, :target, :reflection 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 0ba03338f6..6caadb4ce8 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -33,10 +33,10 @@ module ActiveRecord reload end - if owner.new_record? + if null_scope? # Cache the proxy separately before the owner has an id # or else a post-save proxy will still lack the id - @new_record_proxy ||= CollectionProxy.create(klass, self) + @null_proxy ||= CollectionProxy.create(klass, self) else @proxy ||= CollectionProxy.create(klass, self) end @@ -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 @@ -432,8 +434,7 @@ module ActiveRecord def get_records if reflection.scope_chain.any?(&:any?) || scope.eager_loading? || - klass.current_scope || - klass.default_scopes.any? + klass.scope_attributes? return scope.to_a 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/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index c44242a0f0..58d0f7d65d 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -41,8 +41,7 @@ module ActiveRecord def get_records if reflection.scope_chain.any?(&:any?) || scope.eager_loading? || - klass.current_scope || - klass.default_scopes.any? + klass.scope_attributes? return scope.limit(1).to_a end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cc03e37a12..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 # @@ -258,7 +257,7 @@ module ActiveRecord #:nodoc: # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * RecordInvalid - raised by save! and create! when the record is invalid. + # * RecordInvalid - raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal # nothing was found, please check its documentation for further details. @@ -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/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 42ad285340..42c794c828 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -136,7 +136,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' # supports savepoints. # @@ -189,7 +189,7 @@ module ActiveRecord # semantics of these different levels: # # * http://www.postgresql.org/docs/9.1/static/transaction-iso.html - # * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + # * https://dev.mysql.com/doc/refman/5.6/en/set-transaction.html # # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: # 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 d42f9a894b..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') @@ -652,7 +676,7 @@ module ActiveRecord alias :add_belongs_to :add_reference # Removes the reference(s). Also removes a +type+ column if one exists. - # <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. + # <tt>remove_reference</tt> and <tt>remove_belongs_to</tt> are acceptable. # # ====== Remove the reference # @@ -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/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index c74f4e8fa5..295a7bed87 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -44,11 +44,12 @@ module ActiveRecord attr_reader :connection, :state, :records, :savepoint_name attr_writer :joinable - def initialize(connection, options) + def initialize(connection, options, run_commit_callbacks: false) @connection = connection @state = TransactionState.new @records = [] @joinable = options.fetch(:joinable, true) + @run_commit_callbacks = run_commit_callbacks end def add_record(record) @@ -75,18 +76,21 @@ module ActiveRecord end def before_commit_records - records.uniq.each(&:before_committed!) + records.uniq.each(&:before_committed!) if @run_commit_callbacks end def commit_records ite = records.uniq while record = ite.shift - record.committed! + if @run_commit_callbacks + record.committed! + else + # if not running callbacks, only adds the record to the parent transaction + record.add_to_transaction + end end ensure - ite.each do |i| - i.committed!(should_run_callbacks: false) - end + ite.each { |i| i.committed!(should_run_callbacks: false) } end def full_rollback?; true; end @@ -97,8 +101,8 @@ module ActiveRecord class SavepointTransaction < Transaction - def initialize(connection, savepoint_name, options) - super(connection, options) + def initialize(connection, savepoint_name, options, *args) + super(connection, options, *args) if options[:isolation] raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" end @@ -120,7 +124,7 @@ module ActiveRecord class RealTransaction < Transaction - def initialize(connection, options) + def initialize(connection, options, *args) super if options[:isolation] connection.begin_isolated_db_transaction(options[:isolation]) @@ -147,11 +151,13 @@ module ActiveRecord end def begin_transaction(options = {}) + run_commit_callbacks = !current_transaction.joinable? transaction = if @stack.empty? - RealTransaction.new(@connection, options) + RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) else - SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options) + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, + run_commit_callbacks: run_commit_callbacks) end @stack.push(transaction) @@ -159,18 +165,11 @@ module ActiveRecord end def commit_transaction - inner_transaction = @stack.pop - - if current_transaction.joinable? - inner_transaction.commit - inner_transaction.records.each do |r| - r.add_to_transaction - end - else - inner_transaction.before_commit_records - inner_transaction.commit - inner_transaction.commit_records - end + transaction = @stack.last + transaction.before_commit_records + @stack.pop + transaction.commit + transaction.commit_records end def rollback_transaction(transaction = nil) 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 9625fcf9b8..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 @@ -785,7 +838,9 @@ module ActiveRecord subselect = Arel::SelectManager.new(select.engine) subselect.project Arel.sql(key.name) - subselect.from subsubselect.as('__active_record_temp') + # Materialized subquery by adding distinct + # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' + subselect.from subsubselect.distinct.as('__active_record_temp') end def add_index_length(option_strings, column_names, options = {}) @@ -903,8 +958,7 @@ module ActiveRecord def configure_connection variables = @config.fetch(:variables, {}).stringify_keys - # By default, MySQL 'where id is null' selects the last inserted id. - # Turn this off. http://dev.rubyonrails.org/ticket/6778 + # By default, MySQL 'where id is null' selects the last inserted id; Turn this off. variables['sql_auto_is_null'] = 0 # Increase timeout so the server doesn't disconnect us. @@ -913,14 +967,14 @@ module ActiveRecord variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout) # Make MySQL reject illegal values rather than truncating or blanking them, see - # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables + # http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. unless variables.has_key?('sql_mode') variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see - # http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430 + # http://dev.mysql.com/doc/refman/5.6/en/set-statement.html#id944430 # (trailing comma because variable_assignments will always have content) if @config[:encoding] encoding = "NAMES #{@config[:encoding]}" 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 64985ee933..45b935f1d6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -56,9 +56,9 @@ module ActiveRecord # * <tt>:password</tt> - Defaults to nothing. # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). - # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html). + # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html). + # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html) + # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/set-statement.html). # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb deleted file mode 100644 index 1b74c039ce..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ /dev/null @@ -1,93 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module ArrayParser # :nodoc: - - DOUBLE_QUOTE = '"' - BACKSLASH = "\\" - COMMA = ',' - BRACKET_OPEN = '{' - BRACKET_CLOSE = '}' - - def parse_pg_array(string) # :nodoc: - local_index = 0 - array = [] - while(local_index < string.length) - case string[local_index] - when BRACKET_OPEN - local_index,array = parse_array_contents(array, string, local_index + 1) - when BRACKET_CLOSE - return array - end - local_index += 1 - end - - array - end - - private - - def parse_array_contents(array, string, index) - is_escaping = false - is_quoted = false - was_quoted = false - current_item = '' - - local_index = index - while local_index - token = string[local_index] - if is_escaping - current_item << token - is_escaping = false - else - if is_quoted - case token - when DOUBLE_QUOTE - is_quoted = false - was_quoted = true - when BACKSLASH - is_escaping = true - else - current_item << token - end - else - case token - when BACKSLASH - is_escaping = true - when COMMA - add_item_to_array(array, current_item, was_quoted) - current_item = '' - was_quoted = false - when DOUBLE_QUOTE - is_quoted = true - when BRACKET_OPEN - internal_items = [] - local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) - array.push(internal_items) - when BRACKET_CLOSE - add_item_to_array(array, current_item, was_quoted) - return local_index,array - else - current_item << token - end - end - end - - local_index += 1 - end - return local_index,array - end - - def add_item_to_array(array, current_item, quoted) - return if !quoted && current_item.length == 0 - - if !quoted && current_item == 'NULL' - array.push nil - else - array.push current_item - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index fb4e0de2a8..3de794f797 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -5,29 +5,20 @@ module ActiveRecord class Array < Type::Value # :nodoc: include Type::Helpers::Mutable - # Loads pg_array_parser if available. String parsing can be - # performed quicker by a native extension, which will not create - # a large amount of Ruby objects that will need to be garbage - # collected. pg_array_parser has a C and Java extension - begin - require 'pg_array_parser' - include PgArrayParser - rescue LoadError - require 'active_record/connection_adapters/postgresql/array_parser' - include PostgreSQL::ArrayParser - end - attr_reader :subtype, :delimiter - delegate :type, :user_input_in_time_zone, to: :subtype + delegate :type, :user_input_in_time_zone, :limit, to: :subtype def initialize(subtype, delimiter = ',') @subtype = subtype @delimiter = delimiter + + @pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter + @pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter end def deserialize(value) if value.is_a?(::String) - type_cast_array(parse_pg_array(value), :deserialize) + type_cast_array(@pg_decoder.decode(value), :deserialize) else super end @@ -35,14 +26,14 @@ module ActiveRecord def cast(value) if value.is_a?(::String) - value = parse_pg_array(value) + value = @pg_decoder.decode(value) end type_cast_array(value, :cast) end def serialize(value) if value.is_a?(::Array) - cast_value_for_database(value) + @pg_encoder.encode(type_cast_array(value, :serialize)) else super end @@ -63,41 +54,6 @@ module ActiveRecord @subtype.public_send(method, value) end end - - def cast_value_for_database(value) - if value.is_a?(::Array) - casted_values = value.map { |item| cast_value_for_database(item) } - "{#{casted_values.join(delimiter)}}" - else - quote_and_escape(subtype.serialize(value)) - end - end - - ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays - - def quote_and_escape(value) - case value - when ::String - if string_requires_quoting?(value) - value = value.gsub(/\\/, ARRAY_ESCAPE) - value.gsub!(/"/,"\\\"") - %("#{value}") - else - value - end - when nil then "NULL" - else value - end - end - - # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO - # for a list of all cases in which strings will be quoted. - def string_requires_quoting?(string) - string.empty? || - string == "NULL" || - string =~ /[\{\}"\\\s]/ || - string.include?(delimiter) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb index b3b610a5f6..91d339f32c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -7,7 +7,9 @@ module ActiveRecord :enum end - def cast(value) + private + + def cast_value(value) value.to_s end 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 9b3de41fab..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,11 +15,11 @@ 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' } - composites, nodes = nodes.partition { |row| row['typelem'] != '0' } + 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) } enums.each { |row| register_enum_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/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb index c1835380f9..44a7338bf5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -14,7 +14,7 @@ module ActiveRecord transaction(requires_new: true) do execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) end - rescue => e + rescue ActiveRecord::ActiveRecordError => e original_exception = e end @@ -34,10 +34,10 @@ Rails needs superuser privileges to disable referential integrity. end begin - transaction(require_new: true) do + transaction(requires_new: true) do execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) end - rescue + rescue ActiveRecord::ActiveRecordError end else yield 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 eeb141dd1e..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 @@ -129,8 +129,8 @@ module ActiveRecord result.map do |row| index_name = row[0] - unique = row[1] == 't' - indkey = row[2].split(" ") + unique = row[1] + indkey = row[2].split(" ").map(&:to_i) inddef = row[3] oid = row[4] @@ -164,7 +164,7 @@ module ActiveRecord type_metadata = fetch_type_metadata(column_name, type, oid, fmod) default_value = extract_value_from_default(default) default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, type_metadata, notnull == 'f', default_function) + new_column(column_name, default_value, type_metadata, !notnull, default_function) end 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 92f470ae70..332ac9d88c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,3 +1,7 @@ +# Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility +gem 'pg', '~> 0.18' +require 'pg' + require "active_record/connection_adapters/abstract_adapter" require "active_record/connection_adapters/postgresql/column" require "active_record/connection_adapters/postgresql/database_statements" @@ -12,10 +16,6 @@ require "active_record/connection_adapters/statement_pool" require 'arel/visitors/bind_visitor' -# Make sure we're using pg high enough for PGResult#values -gem 'pg', '~> 0.15' -require 'pg' - require 'ipaddr' module ActiveRecord @@ -278,8 +278,7 @@ module ActiveRecord @table_alias_length = nil connect - add_pg_decoders - + add_pg_encoders @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) @@ -287,6 +286,8 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end + add_pg_decoders + @type_map = Type::HashLookupTypeMap.new initialize_type_map(type_map) @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] @@ -567,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/ @@ -593,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 @@ -608,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: @@ -798,11 +803,20 @@ module ActiveRecord ) end_sql execute_and_clear(sql, "SCHEMA", []) do |result| - result.getvalue(0, 0) == 't' + result.getvalue(0, 0) end end end + def add_pg_encoders + map = PG::TypeMapByClass.new + map[Integer] = PG::TextEncoder::Integer.new + map[TrueClass] = PG::TextEncoder::Boolean.new + map[FalseClass] = PG::TextEncoder::Boolean.new + map[Float] = PG::TextEncoder::Float.new + @connection.type_map_for_queries = map + end + def add_pg_decoders coders_by_name = { 'int2' => PG::TextDecoder::Integer, @@ -813,13 +827,15 @@ module ActiveRecord '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 + 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 - .map { |row| construct_coder(row, coders_by_name['typname']) } + .map { |row| construct_coder(row, coders_by_name[row['typname']]) } .compact end @@ -830,7 +846,7 @@ module ActiveRecord def construct_coder(row, coder_class) return unless coder_class - coder_class.new(oid: row['oid'], name: row['typname']) + coder_class.new(oid: row['oid'].to_i, name: row['typname']) end ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgresql) diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 97ce4642aa..1ad910c4bc 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -85,6 +85,24 @@ module ActiveRecord mattr_accessor :dump_schema_after_migration, instance_writer: false self.dump_schema_after_migration = true + ## + # :singleton-method: + # Specifies which database schemas to dump when calling db:structure:dump. + # If :schema_search_path (the default), it will dumps any schemas listed in schema_search_path. + # Use :all to always dumps all schemas regardless of the schema_search_path. + # A string of comma separated schemas can also be used to pass a custom list of schemas. + mattr_accessor :dump_schemas, instance_writer: false + self.dump_schemas = :schema_search_path + + ## + # :singleton-method: + # 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 + mattr_accessor :maintain_test_schema, instance_accessor: false mattr_accessor :belongs_to_required_by_default, instance_accessor: false @@ -123,8 +141,7 @@ module ActiveRecord return super unless ids.length == 1 return super if block_given? || primary_key.nil? || - default_scopes.any? || - current_scope || + scope_attributes? || columns_hash.include?(inheritance_column) || ids.first.kind_of?(Array) @@ -152,8 +169,7 @@ module ActiveRecord end def find_by(*args) # :nodoc: - return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any? - return super if default_scopes.any? + return super if scope_attributes? || !(Hash === args.first) || reflect_on_all_aggregations.any? hash = args.first @@ -286,20 +302,25 @@ 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) @attributes = coder['attributes'] init_internals @@ -308,8 +329,8 @@ module ActiveRecord self.class.define_attribute_methods - _run_find_callbacks - _run_initialize_callbacks + run_callbacks :find + run_callbacks :initialize self end @@ -345,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 @@ -370,6 +391,7 @@ module ActiveRecord coder['raw_attributes'] = attributes_before_type_cast coder['attributes'] = @attributes coder['new_record'] = new_record? + coder['active_record_yaml_version'] = 1 end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ 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/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 18775caad2..2c1771dd6c 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -139,13 +139,13 @@ module ActiveRecord # name: kitten.png # sha: <%= file_sha 'files/kitten.png' %> # - # = Transactional Fixtures + # = Transactional Tests # # Test cases can use begin+rollback to isolate their changes to the database instead of having to # delete+insert for every test case. # # class FooTest < ActiveSupport::TestCase - # self.use_transactional_fixtures = true + # self.use_transactional_tests = true # # test "godzilla" do # assert !Foo.all.empty? @@ -159,14 +159,14 @@ module ActiveRecord # end # # If you preload your test database with all fixture data (probably in the rake task) and use - # transactional fixtures, then you may omit all fixtures declarations in your test cases since + # transactional tests, then you may omit all fixtures declarations in your test cases since # all the data's already there and every case rolls back its changes. # # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to # true. This will provide access to fixture data for every table that has been loaded through # fixtures (depending on the value of +use_instantiated_fixtures+). # - # When *not* to use transactional fixtures: + # When *not* to use transactional tests: # # 1. You're testing whether a transaction works correctly. Nested transactions don't commit until # all parent transactions commit, particularly, the fixtures transaction which is begun in setup @@ -835,13 +835,15 @@ module ActiveRecord class_attribute :fixture_path, :instance_writer => false class_attribute :fixture_table_names class_attribute :fixture_class_names + class_attribute :use_transactional_tests class_attribute :use_transactional_fixtures class_attribute :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures class_attribute :config + singleton_class.deprecate 'use_transactional_fixtures=' => 'use use_transactional_tests= instead' + self.fixture_table_names = [] - self.use_transactional_fixtures = true self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false self.config = ActiveRecord::Base @@ -849,6 +851,16 @@ module ActiveRecord self.fixture_class_names = Hash.new do |h, fixture_set_name| h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config) end + + silence_warnings do + define_singleton_method :use_transactional_tests do + if use_transactional_fixtures.nil? + true + else + use_transactional_fixtures + end + end + end end module ClassMethods @@ -919,13 +931,13 @@ module ActiveRecord end def run_in_transaction? - use_transactional_fixtures && + use_transactional_tests && !self.class.uses_transaction?(method_name) end def setup_fixtures(config = ActiveRecord::Base) - if pre_loaded_fixtures && !use_transactional_fixtures - raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' + if pre_loaded_fixtures && !use_transactional_tests + raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_tests' end @fixture_cache = {} diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb new file mode 100644 index 0000000000..89dee58423 --- /dev/null +++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb @@ -0,0 +1,46 @@ +module ActiveRecord + module LegacyYamlAdapter + def self.convert(klass, coder) + return coder unless coder.is_a?(Psych::Coder) + + case coder["active_record_yaml_version"] + when 1 then coder + else + if coder["attributes"].is_a?(AttributeSet) + Rails420.convert(klass, coder) + else + Rails41.convert(klass, coder) + end + end + end + + module Rails420 + def self.convert(klass, coder) + attribute_set = coder["attributes"] + + klass.attribute_names.each do |attr_name| + attribute = attribute_set[attr_name] + if attribute.type.is_a?(Delegator) + type_from_klass = klass.type_for_attribute(attr_name) + attribute_set[attr_name] = attribute.with_type(type_from_klass) + end + end + + coder + end + end + + module Rails41 + def self.convert(klass, coder) + attributes = klass.attributes_builder + .build_from_database(coder["attributes"]) + new_record = coder["attributes"][klass.primary_key].blank? + + { + "attributes" => attributes, + "new_record" => new_record, + } + end + end + end +end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index ff7102d35b..3d95c54ef3 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -51,7 +51,7 @@ module ActiveRecord # end # # Database-specific information on row locking: - # MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html + # MySQL: http://dev.mysql.com/doc/refman/5.6/en/innodb-locking-reads.html # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE module Pessimistic # Obtain a row lock on this record. Reloads the record to obtain the requested 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/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index fa6eaa7e64..74894d0c37 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -14,7 +14,7 @@ module ActiveRecord 0 end - def update_all(_updates, _conditions = nil, _options = {}) + def update_all(_updates) 0 end @@ -80,7 +80,7 @@ module ActiveRecord end end - def exists?(_id = false) + def exists?(_conditions = :none) false end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a6176dffdb..ae1c326d95 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -462,9 +462,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 +477,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/railtie.rb b/activerecord/lib/active_record/railtie.rb index b6de90e89d..7e907beec0 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -58,9 +58,6 @@ module ActiveRecord require "active_record/railties/console_sandbox" if app.sandbox? require "active_record/base" console = ActiveSupport::Logger.new(STDERR) - console.formatter = Rails.logger.formatter - console.level = Rails.logger.level - Rails.logger.extend ActiveSupport::Logger.broadcast console end @@ -105,6 +102,14 @@ module ActiveRecord end end + initializer "active_record.warn_on_records_fetched_greater_than" do + if config.active_record.warn_on_records_fetched_greater_than + ActiveSupport.on_load(:active_record) do + require 'active_record/relation/record_fetch_warning' + end + end + end + initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do app.config.active_record.each do |k,v| 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/reflection.rb b/activerecord/lib/active_record/reflection.rb index dab5a502a5..1b0ae2c942 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -166,8 +166,8 @@ module ActiveRecord # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. # # MacroReflection + # AggregateReflection # AssociationReflection - # AggregateReflection # HasManyReflection # HasOneReflection # BelongsToReflection @@ -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 @@ -370,6 +370,12 @@ module ActiveRecord [self] end + # This is for clearing cache on the reflection. Useful for tests that need to compare + # SQL queries on associations. + def clear_association_scope_cache # :nodoc: + @association_scope_cache.clear + end + def nested? false end @@ -629,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 @@ -694,7 +700,7 @@ module ActiveRecord def chain @chain ||= begin a = source_reflection.chain - b = through_reflection.chain + b = through_reflection.chain.map(&:dup) if options[:source_type] b[0] = PolymorphicReflection.new(b[0], self) @@ -706,6 +712,15 @@ module ActiveRecord end end + # This is for clearing cache on the reflection. Useful for tests that need to compare + # SQL queries on associations. + def clear_association_scope_cache # :nodoc: + @chain = nil + delegate_reflection.clear_association_scope_cache + source_reflection.clear_association_scope_cache + through_reflection.clear_association_scope_cache + end + # Consider the following example: # # class Person diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 6bbec7c0c0..85648a7f8f 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -205,7 +205,9 @@ module ActiveRecord # constraint an exception may be raised, just retry: # # begin - # CreditAccount.find_or_create_by(user_id: user.id) + # CreditAccount.transaction(requires_new: true) do + # CreditAccount.find_or_create_by(user_id: user.id) + # end # rescue ActiveRecord::RecordNotUnique # retry # end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 7f27e7b463..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) @@ -182,7 +184,7 @@ module ActiveRecord private def has_include?(column_name) - eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?)) + eager_loading? || (includes_values.present? && column_name && column_name != :all) end def perform_calculation(operation, column_name) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 7bd091b66c..6a3a56f1cc 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. @@ -378,7 +376,7 @@ module ActiveRecord def construct_relation_for_association_calculations from = arel.froms.first if Arel::Table === from - apply_join_dependency(self, construct_join_dependency) + apply_join_dependency(self, construct_join_dependency(joins_values)) else # FIXME: as far as I can tell, `from` will always be an Arel::Table. # There are no tests that test this branch, but presumably it's diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb new file mode 100644 index 0000000000..14e1bf89fa --- /dev/null +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -0,0 +1,49 @@ +module ActiveRecord + class Relation + module RecordFetchWarning + # When this module is prepended to ActiveRecord::Relation and + # `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 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. + # See active_record/lib/relation/batches.rb for more information. + def exec_queries + QueryRegistry.reset + + super.tap do + if logger && warn_on_records_fetched_greater_than + if @records.length > warn_on_records_fetched_greater_than + logger.warn "Query fetched #{@records.size} #{@klass} records: #{QueryRegistry.queries.join(";")}" + end + end + end + end + + ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| + payload = args.last + + QueryRegistry.queries << payload[:sql] + end + + class QueryRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + attr_accessor :queries + + def initialize + reset + end + + def reset + @queries = [] + end + end + end + end +end + +ActiveRecord::Relation.prepend ActiveRecord::Relation::RecordFetchWarning 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.rb b/activerecord/lib/active_record/scoping.rb index fca4f1c9d3..f049b658c4 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -17,6 +17,17 @@ module ActiveRecord def current_scope=(scope) #:nodoc: ScopeRegistry.set_value_for(:current_scope, self.to_s, scope) end + + # Collects attributes from scopes that should be applied when creating + # an AR instance for the particular class this is called on. + def scope_attributes # :nodoc: + all.scope_for_create + end + + # Are there attributes associated with this scope? + def scope_attributes? # :nodoc: + current_scope + end end def populate_with_current_scope_attributes diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 18190cb535..3590b8846e 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -33,6 +33,11 @@ module ActiveRecord block_given? ? relation.scoping { yield } : relation end + # Are there attributes associated with this scope? + def scope_attributes? # :nodoc: + super || default_scopes.any? || respond_to?(:default_scope) + end + def before_remove_const #:nodoc: self.current_scope = nil end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 43c7b1c574..7b62626896 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -39,17 +39,6 @@ module ActiveRecord end end - # Collects attributes from scopes that should be applied when creating - # an AR instance for the particular class this is called on. - def scope_attributes # :nodoc: - all.scope_for_create - end - - # Are there default attributes associated with this scope? - def scope_attributes? # :nodoc: - current_scope || default_scopes.any? - end - # Adds a class method for retrieving and querying objects. A \scope # represents a narrowing of a database query, such as # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>. diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index ce1de4b76e..d7da95c8a9 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -46,7 +46,15 @@ module ActiveRecord def structure_dump(filename) set_psql_env - search_path = configuration['schema_search_path'] + + search_path = case ActiveRecord::Base.dump_schemas + when :schema_search_path + configuration['schema_search_path'] + when :all + nil + when String + ActiveRecord::Base.dump_schemas + end unless search_path.blank? search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") end @@ -59,7 +67,7 @@ module ActiveRecord def structure_load(filename) set_psql_env - Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}") + Kernel.system("psql -X -q -f #{Shellwords.escape(filename)} #{configuration['database']}") end private 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 ffe7c4ae42..311dacb449 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -227,8 +227,6 @@ module ActiveRecord # after_commit :do_foo_bar, on: [:create, :update] # after_commit :do_bar_baz, on: [:update, :destroy] # - # Note that transactional fixtures do not play well with this feature. Please - # use the +test_after_commit+ gem to have these hooks fired in tests. def after_commit(*args, &block) set_options_for_callbacks!(args) set_callback(:commit, :after, *args, &block) @@ -321,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. @@ -331,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 @@ -342,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) @@ -356,6 +354,7 @@ module ActiveRecord if has_transactional_callbacks? self.class.connection.add_transaction_record(self) else + sync_with_transaction_state set_transaction_state(self.class.connection.transaction_state) end remember_transaction_record_state @@ -413,13 +412,14 @@ module ActiveRecord transaction_level = (@_start_transaction_state[:level] || 0) - 1 if transaction_level < 1 || force restore_state = @_start_transaction_state - thaw unless restore_state[:frozen?] + thaw @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] pk = self.class.primary_key if pk && read_attribute(pk) != restore_state[:id] write_attribute(pk, restore_state[:id]) end + freeze if restore_state[:frozen?] end end end @@ -454,13 +454,13 @@ module ActiveRecord end # Updates the attributes on this particular ActiveRecord object so that - # if it is associated with a transaction, then the state of the AR object - # will be updated to reflect the current state of the transaction + # if it's associated with a transaction, then the state of the ActiveRecord + # object will be updated to reflect the current state of the transaction # # The @transaction_state variable stores the states of the associated # transaction. This relies on the fact that a transaction can only be in # one rollback or commit (otherwise a list of states would be required) - # Each AR object inside of a transaction carries that transaction's + # Each ActiveRecord object inside of a transaction carries that transaction's # TransactionState. # # This method checks to see if the ActiveRecord object's state reflects 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/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 732029c723..ea3e0d6a45 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -26,9 +26,15 @@ module ActiveRecord end end + def inspect + Kernel.instance_method(:inspect).bind(self).call + end + def changed_in_place?(raw_old_value, value) return false if value.nil? - subtype.changed_in_place?(raw_old_value, serialize(value)) + raw_new_value = serialize(value) + raw_old_value.nil? != raw_new_value.nil? || + subtype.changed_in_place?(raw_old_value, raw_new_value) end def accessor 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) |