diff options
Diffstat (limited to 'activerecord/lib/active_record')
35 files changed, 384 insertions, 93 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 3c92e379f1..5e5995f566 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1073,6 +1073,9 @@ module ActiveRecord # with +attributes+, linked to this object through a foreign key, and that has already # been saved (if it passed the validation). *Note*: This only works if the base model # already exists in the DB, not if it is a new (unsaved) record! + # [collection.create!(attributes = {})] + # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # if the record is invalid. # # (*Note*: +collection+ is replaced with the symbol passed as the first argument, so # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.) @@ -1094,6 +1097,7 @@ module ActiveRecord # * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>) # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) + # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>) # The declaration can also include an options hash to specialize the behavior of the association. # # === Options diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index f3f4792eaa..db0553ea76 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -30,7 +30,7 @@ module ActiveRecord reset_scope end - # Returns the name of the table of the related class: + # Returns the name of the table of the associated class: # # post.comments.aliased_table_name # => "comments" # @@ -236,6 +236,7 @@ module ActiveRecord skip_assign = [reflection.foreign_key, reflection.type].compact attributes = create_scope.except(*(record.changed - skip_assign)) record.assign_attributes(attributes) + set_inverse_instance(record) end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index a9525436fb..aa5551fe0c 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -101,6 +101,7 @@ module ActiveRecord scope.includes! item.includes_values scope.where_values += item.where_values + scope.order_values |= item.order_values end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 9ac561b997..3ba6a71366 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -66,8 +66,19 @@ module ActiveRecord::Associations::Builder def add_touch_callbacks(reflection) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def belongs_to_touch_after_save_or_destroy_for_#{name} - record = #{name} + foreign_key_field = #{reflection.foreign_key.inspect} + old_foreign_id = attribute_was(foreign_key_field) + + if old_foreign_id + reflection_klass = #{reflection.klass} + old_record = reflection_klass.find_by(reflection_klass.primary_key => old_foreign_id) + + if old_record + old_record.touch #{options[:touch].inspect if options[:touch] != true} + end + end + record = #{name} unless record.nil? || record.new_record? record.touch #{options[:touch].inspect if options[:touch] != true} end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index c2add32aa6..56e57cc36e 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -92,7 +92,7 @@ module ActiveRecord # # => ActiveModel::MissingAttributeError: missing attribute: person_id # # *Second:* You can pass a block so it can be used just like Array#select. - # This build an array of objects from the database for the scope, + # This builds an array of objects from the database for the scope, # converting them into an array and iterating through them using # Array#select. # @@ -228,6 +228,7 @@ module ActiveRecord def build(attributes = {}, &block) @association.build(attributes, &block) end + alias_method :new, :build # Returns a new object of the collection type that has been instantiated with # attributes, linked to this object and that has already been saved (if it @@ -303,7 +304,7 @@ module ActiveRecord @association.concat(*records) end - # Replace this collection with +other_array+. This will perform a diff + # Replaces this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. # # class Person < ActiveRecord::Base @@ -832,8 +833,6 @@ module ActiveRecord @association.include?(record) end - alias_method :new, :build - def proxy_association @association end @@ -848,10 +847,8 @@ module ActiveRecord # Returns a <tt>Relation</tt> object for the records in this association def scope - association = @association - - @association.scope.extending! do - define_method(:proxy_association) { association } + @association.scope.tap do |scope| + scope.proxy_association = @association end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index e589959cf2..28e081c03c 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -5,10 +5,31 @@ module ActiveRecord autoload :JoinBase, 'active_record/associations/join_dependency/join_base' autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association' - attr_reader :join_parts, :reflections, :alias_tracker, :active_record - + attr_reader :join_parts, :reflections, :alias_tracker, :base_klass + + # base is the base class on which operation is taking place. + # associations is the list of associations which are joined using hash, symbol or array. + # joins is the list of all string join commnads and arel nodes. + # + # Example : + # + # class Physician < ActiveRecord::Base + # has_many :appointments + # has_many :patients, through: :appointments + # end + # + # If I execute `@physician.patients.to_a` then + # base #=> Physician + # associations #=> [] + # joins #=> [#<Arel::Nodes::InnerJoin: ...] + # + # However if I execute `Physician.joins(:appointments).to_a` then + # base #=> Physician + # associations #=> [:appointments] + # joins #=> [] + # def initialize(base, associations, joins) - @active_record = base + @base_klass = base @table_joins = joins @join_parts = [JoinBase.new(base)] @associations = {} @@ -54,10 +75,12 @@ module ActiveRecord parent }.uniq - remove_duplicate_results!(active_record, records, @associations) + remove_duplicate_results!(base_klass, records, @associations) records end + protected + def remove_duplicate_results!(base, records, associations) case associations when Symbol, String @@ -88,8 +111,6 @@ module ActiveRecord end end - protected - def cache_joined_association(association) associations = [] parent = association.parent @@ -109,7 +130,7 @@ module ActiveRecord case associations when Symbol, String reflection = parent.reflections[associations.intern] or - raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" + raise ConfigurationError, "Association named '#{ associations }' was not found on #{ parent.base_klass.name }; perhaps you misspelled it?" unless join_association = find_join_association(reflection, parent) @reflections << reflection join_association = build_join_association(reflection, parent) diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 0d3b4dbab1..e4d17451dc 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -55,14 +55,19 @@ module ActiveRecord def find_parent_in(other_join_dependency) other_join_dependency.join_parts.detect do |join_part| - parent == join_part + case parent + when JoinBase + parent.base_klass == join_part.base_klass + else + parent == join_part + end end end - def join_to(relation) + def join_to(manager) tables = @tables.dup foreign_table = parent_table - foreign_klass = parent.active_record + foreign_klass = parent.base_klass # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse @@ -75,7 +80,7 @@ module ActiveRecord foreign_key = reflection.foreign_key when :has_and_belongs_to_many # Join the join table first... - relation.from(join( + manager.from(join( table, table[reflection.foreign_key]. eq(foreign_table[reflection.active_record_primary_key]) @@ -109,15 +114,30 @@ module ActiveRecord constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty? end - relation.from(join(table, constraint)) + manager.from(join(table, constraint)) # The current table in this iteration becomes the foreign table in the next foreign_table, foreign_klass = table, reflection.klass end - relation + manager end + # Builds equality condition. + # + # Example: + # + # class Physician < ActiveRecord::Base + # has_many :appointments + # end + # + # If I execute `Physician.joins(:appointments).to_a` then + # reflection #=> #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...> + # table #=> #<Arel::Table @name="appointments" ...> + # key #=> physician_id + # foreign_table #=> #<Arel::Table @name="physicians" ...> + # foreign_key #=> id + # def build_constraint(reflection, table, key, foreign_table, foreign_key) constraint = table[key].eq(foreign_table[foreign_key]) diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb index 3920e84976..a7dacdbfd6 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -4,7 +4,7 @@ module ActiveRecord class JoinBase < JoinPart # :nodoc: def ==(other) other.class == self.class && - other.active_record == active_record + other.base_klass == base_klass end def aliased_prefix @@ -16,7 +16,7 @@ module ActiveRecord end def aliased_table_name - active_record.table_name + base_klass.table_name end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 5604687b57..b534569063 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -1,7 +1,7 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: - # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited + # A JoinPart represents a part of a JoinDependency. It is inherited # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which # everything else is being joined onto. A JoinAssociation represents an association which # is joining to the base. A JoinAssociation may result in more than one actual join @@ -11,12 +11,12 @@ module ActiveRecord # The Active Record class which this join part is associated 'about'; for a JoinBase # this is the actual base model, for a JoinAssociation this is the target model of the # association. - attr_reader :active_record + attr_reader :base_klass - delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record + delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :base_klass - def initialize(active_record) - @active_record = active_record + def initialize(base_klass) + @base_klass = base_klass @cached_record = {} @column_names_with_alias = nil end @@ -70,7 +70,7 @@ module ActiveRecord end def instantiate(row) - @cached_record[record_id(row)] ||= active_record.instantiate(extract_record(row)) + @cached_record[record_id(row)] ||= base_klass.instantiate(extract_record(row)) end end end diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb index 38bc7ce7da..157b627ad5 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -5,9 +5,13 @@ module ActiveRecord include ThroughAssociation def associated_records_by_owner - super.each do |owner, records| - records.uniq! if reflection_scope.distinct_value + records_by_owner = super + + if reflection_scope.distinct_value + records_by_owner.each_value { |records| records.uniq! } end + + records_by_owner end end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 43520142bf..35f29b37a2 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -14,7 +14,7 @@ module ActiveRecord def target_scope scope = super chain[1..-1].each do |reflection| - scope = scope.merge( + scope.merge!( reflection.klass.all.with_default_scope. except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 25d62fdb85..7f1ebab4cd 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -11,6 +11,12 @@ module ActiveRecord end module ClassMethods + ## + # :method: serialized_attributes + # + # Returns a hash of all the attributes that have been specified for + # serialization as keys and their class restriction as values. + # If you have an attribute that needs to be saved to the database as an # object, and retrieved as the same object, then specify the name of that # attribute using this method and it will be handled automatically. The @@ -44,6 +50,7 @@ module ActiveRecord end end + # *DEPRECATED*: Use ActiveRecord::AttributeMethods::Serialization::ClassMethods#serialized_attributes class level method instead. def serialized_attributes message = "Instance level serialized_attributes method is deprecated, please use class level method." ActiveSupport::Deprecation.warn message @@ -86,10 +93,10 @@ module ActiveRecord # This is only added to the model when serialize is called, which # ensures we do not make things slower when serialization is not used. - module Behavior #:nodoc: + module Behavior # :nodoc: extend ActiveSupport::Concern - module ClassMethods + module ClassMethods # :nodoc: def initialize_attributes(attributes, options = {}) serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized super(attributes, options) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index 14ef07a75e..a9ef11aa83 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -26,6 +26,15 @@ module ActiveRecord end end + def string_to_bit(value) + case value + when /^0x/i + value[2..-1].hex.to_s(2) # Hexadecimal notation + else + value # Bit-string notation + end + end + def hstore_to_string(object) if Hash === object object.map { |k,v| diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 51f377dfd7..1be116ce10 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -18,8 +18,19 @@ module ActiveRecord end end + class Bit < Type + def type_cast(value) + if String === value + ConnectionAdapters::PostgreSQLColumn.string_to_bit value + else + value + end + end + end + class Bytea < Type def type_cast(value) + return if value.nil? PGconn.unescape_bytea value end end @@ -322,14 +333,14 @@ module ActiveRecord # FIXME: why are we keeping these types as strings? alias_type 'tsvector', 'text' alias_type 'interval', 'text' - alias_type 'bit', 'text' - alias_type 'varbit', 'text' alias_type 'macaddr', 'text' alias_type 'uuid', 'text' register_type 'money', OID::Money.new register_type 'bytea', OID::Bytea.new register_type 'bool', OID::Boolean.new + register_type 'bit', OID::Bit.new + register_type 'varbit', OID::Bit.new register_type 'float4', OID::Float.new alias_type 'float8', 'float4' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e34e1fc10c..bf403c3ae0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -80,7 +80,7 @@ module ActiveRecord when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m $1 # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?)\z/ + when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ $1 # Character types when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 1e03414c29..a1943dfcb0 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -54,11 +54,11 @@ module ActiveRecord end def connection_id - Thread.current['ActiveRecord::Base.connection_id'] + ActiveRecord::RuntimeRegistry.connection_id end def connection_id=(connection_id) - Thread.current['ActiveRecord::Base.connection_id'] = connection_id + ActiveRecord::RuntimeRegistry.connection_id = connection_id end # Returns the configuration of the associated connection as a hash: diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index db5e7b82ca..733d4e1c67 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -80,11 +80,11 @@ module ActiveRecord class_attribute :default_connection_handler, instance_writer: false def self.connection_handler - Thread.current[:active_record_connection_handler] || self.default_connection_handler + ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler end def self.connection_handler=(handler) - Thread.current[:active_record_connection_handler] = handler + ActiveRecord::RuntimeRegistry.connection_handler = handler end self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new @@ -344,6 +344,10 @@ module ActiveRecord self.class.connection end + def connection_handler + self.class.connection_handler + end + # Returns the contents of the record as a nicely formatted string. def inspect inspection = if @attributes diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 15736575a2..e65dab07ba 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,22 +1,22 @@ require 'active_support/lazy_load_hooks' +require 'active_record/explain_registry' module ActiveRecord module Explain - # Relation#explain needs to be able to collect the queries. + # Executes the block with the collect flag enabled. Queries are collected + # asynchronously by the subscriber and returned. def collecting_queries_for_explain # :nodoc: - current = Thread.current - original, current[:available_queries_for_explain] = current[:available_queries_for_explain], [] + ExplainRegistry.collect = true yield - return current[:available_queries_for_explain] + ExplainRegistry.queries ensure - # Note that the return value above does not depend on this assignment. - current[:available_queries_for_explain] = original + ExplainRegistry.reset end # Makes the adapter execute EXPLAIN for the tuples of queries and bindings. # Returns a formatted string ready to be logged. def exec_explain(queries) # :nodoc: - str = queries && queries.map do |sql, bind| + str = queries.map do |sql, bind| [].tap do |msg| msg << "EXPLAIN for: #{sql}" unless bind.empty? @@ -31,6 +31,7 @@ module ActiveRecord def str.inspect self end + str end end diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb new file mode 100644 index 0000000000..f5cd57e075 --- /dev/null +++ b/activerecord/lib/active_record/explain_registry.rb @@ -0,0 +1,30 @@ +require 'active_support/per_thread_registry' + +module ActiveRecord + # This is a thread locals registry for EXPLAIN. For example + # + # ActiveRecord::ExplainRegistry.queries + # + # returns the collected queries local to the current thread. + # + # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # for further details. + class ExplainRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + attr_accessor :queries, :collect + + def initialize + reset + end + + def collect? + @collect + end + + def reset + @collect = false + @queries = [] + end + end +end diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 0f927496fb..a3bc56d600 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -1,4 +1,5 @@ require 'active_support/notifications' +require 'active_record/explain_registry' module ActiveRecord class ExplainSubscriber # :nodoc: @@ -7,8 +8,8 @@ module ActiveRecord end def finish(name, id, payload) - if queries = Thread.current[:available_queries_for_explain] - queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload) + if ExplainRegistry.collect? && !ignore_payload?(payload) + ExplainRegistry.queries << payload.values_at(:sql, :binds) end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index c26fc76515..45dc26f0ed 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -708,11 +708,18 @@ module ActiveRecord module TestFixtures extend ActiveSupport::Concern - included do - setup :setup_fixtures - teardown :teardown_fixtures + def before_setup + setup_fixtures + super + end + + def after_teardown + super + teardown_fixtures + end - class_attribute :fixture_path + included do + class_attribute :fixture_path, :instance_writer => false class_attribute :fixture_table_names class_attribute :fixture_class_names class_attribute :use_transactional_fixtures @@ -765,8 +772,7 @@ module ActiveRecord def try_to_load_dependency(file_name) require_dependency file_name rescue LoadError => e - # Let's hope the developer has included it himself - + # Let's hope the developer has included it # Let's warn in case this is a subdependency, otherwise # subdependency error messages are totally cryptic if ActiveRecord::Base.logger diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index c1ba524c84..61e5c120df 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -3,11 +3,11 @@ module ActiveRecord IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"] def self.runtime=(value) - Thread.current[:active_record_sql_runtime] = value + ActiveRecord::RuntimeRegistry.sql_runtime = value end def self.runtime - Thread.current[:active_record_sql_runtime] ||= 0 + ActiveRecord::RuntimeRegistry.sql_runtime ||= 0 end def self.reset_runtime diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index d3edcf3cdb..451104106c 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -466,7 +466,7 @@ module ActiveRecord @connection.respond_to?(:reverting) && @connection.reverting end - class ReversibleBlockHelper < Struct.new(:reverting) + class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc: def up yield unless reverting end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index f0c29bbf73..d607f49e2b 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -362,7 +362,7 @@ module ActiveRecord assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) elsif attributes['id'].present? - raise_nested_attributes_record_not_found(association_name, attributes['id']) + raise_nested_attributes_record_not_found!(association_name, attributes['id']) elsif !reject_new_record?(association_name, attributes) method = "build_#{association_name}" @@ -452,7 +452,7 @@ module ActiveRecord assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) end else - raise_nested_attributes_record_not_found(association_name, attributes['id']) + raise_nested_attributes_record_not_found!(association_name, attributes['id']) end end end @@ -514,7 +514,7 @@ module ActiveRecord end end - def raise_nested_attributes_record_not_found(association_name, record_id) + def raise_nested_attributes_record_not_found!(association_name, record_id) raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 26200e1ba6..fb1bab6dcc 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -166,7 +166,7 @@ db_namespace = namespace :db do end # desc "Raises an error if there are pending migrations" - task :abort_if_pending_migrations => [:environment, :load_config] do + task :abort_if_pending_migrations => :environment do pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations if pending_migrations.any? @@ -347,7 +347,7 @@ db_namespace = namespace :db do end # desc 'Check for pending migrations and load the test schema' - task :prepare => 'db:abort_if_pending_migrations' do + task :prepare do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke end @@ -373,7 +373,7 @@ namespace :railties do puts "NOTE: Migration #{migration.basename} from #{name} has been skipped. Migration with the same name already exists." end - on_copy = Proc.new do |name, migration, old_path| + on_copy = Proc.new do |name, migration| puts "Copied migration #{migration.basename} from #{name}" end @@ -383,5 +383,5 @@ namespace :railties do end end -task 'test:prepare' => 'db:test:prepare' +task 'test:prepare' => ['db:test:prepare', 'db:test:load', 'db:abort_if_pending_migrations'] diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0995750ecd..60eda96f08 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -75,8 +75,13 @@ module ActiveRecord end end - # Abstract base class for AggregateReflection and AssociationReflection. Objects of + # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. + # + # MacroReflection + # AggregateReflection + # AssociationReflection + # ThroughReflection class MacroReflection # Returns the name of the macro. # @@ -401,6 +406,16 @@ module ActiveRecord # has_many :tags, through: :taggings # end # + # class Tagging < ActiveRecord::Base + # belongs_to :post + # belongs_to :tag + # end + # + # tags_reflection = Post.reflect_on_association(:tags) + # + # taggings_reflection = tags_reflection.source_reflection + # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags"> + # def source_reflection @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first end @@ -426,6 +441,17 @@ module ActiveRecord # The chain is built by recursively calling #chain on the source reflection and the through # reflection. The base case for the recursion is a normal association, which just returns # [self] as its #chain. + # + # class Post < ActiveRecord::Base + # has_many :taggings + # has_many :tags, through: :taggings + # end + # + # tags_reflection = Post.reflect_on_association(:tags) + # tags_reflection.chain + # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>, + # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>] + # def chain @chain ||= begin chain = source_reflection.chain + through_reflection.chain @@ -496,9 +522,16 @@ module ActiveRecord source_reflection.options[:primary_key] || primary_key(klass || self.klass) end - # Gets an array of possible <tt>:through</tt> source reflection names: + # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form. + # + # class Post < ActiveRecord::Base + # has_many :taggings + # has_many :tags, through: :taggings + # end # - # [:singularized, :pluralized] + # tags_reflection = Post.reflect_on_association(:tags) + # tags_reflection.source_reflection_names + # # => [:tag, :tags] # def source_reflection_names @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym } diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index c7b6c715f5..56462d355b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -17,7 +17,7 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded - attr_accessor :default_scoped + attr_accessor :default_scoped, :proxy_association alias :model :klass alias :loaded? :loaded alias :default_scoped? :default_scoped diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index eb23e92fb8..936b83261e 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -39,7 +39,7 @@ module ActiveRecord end class Merger # :nodoc: - attr_reader :relation, :values + attr_reader :relation, :values, :other def initialize(relation, other) if other.default_scoped? && other.klass != relation.klass @@ -48,11 +48,12 @@ module ActiveRecord @relation = relation @values = other.values + @other = other end NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS + Relation::MULTI_VALUE_METHODS - - [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc: + [:joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc: def normal_values NORMAL_VALUES @@ -66,12 +67,39 @@ module ActiveRecord merge_multi_values merge_single_values + merge_joins relation end private + def merge_joins + return if values[:joins].blank? + + if other.klass == relation.klass + relation.joins!(*values[:joins]) + else + joins_dependency, rest = values[:joins].partition do |join| + case join + when Hash, Symbol, Array + true + else + false + end + end + + join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass, + joins_dependency, + []) + relation.joins! rest + + join_dependency.join_associations.each do |association| + @relation = association.join_relation(relation) + end + end + end + def merge_multi_values relation.where_values = merged_wheres relation.bind_values = merged_binds diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 257221174b..6ee3711052 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -34,7 +34,6 @@ module ActiveRecord # # User.where.not(name: "Jon", role: "admin") # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' - # def not(opts, *rest) where_value = @scope.send(:build_where, opts, rest).map do |rel| case rel @@ -353,7 +352,7 @@ module ActiveRecord spawn.unscope!(*args) end - def unscope!(*args) + def unscope!(*args) # :nodoc: args.flatten! args.each do |scope| @@ -934,9 +933,7 @@ module ActiveRecord association_joins = buckets[:association_join] || [] stashed_association_joins = buckets[:stashed_join] || [] join_nodes = (buckets[:join_node] || []).uniq - string_joins = (buckets[:string_join] || []).map { |x| - x.strip - }.uniq + string_joins = (buckets[:string_join] || []).map { |x| x.strip }.uniq join_list = join_nodes + custom_join_ast(manager, string_joins) diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb new file mode 100644 index 0000000000..63e6738622 --- /dev/null +++ b/activerecord/lib/active_record/runtime_registry.rb @@ -0,0 +1,17 @@ +require 'active_support/per_thread_registry' + +module ActiveRecord + # This is a thread locals registry for Active Record. For example: + # + # ActiveRecord::RuntimeRegistry.connection_handler + # + # returns the connection handler local to the current thread. + # + # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # for further details. + class RuntimeRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + attr_accessor :connection_handler, :sql_runtime, :connection_id + end +end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 9746b1c3c2..0cf3d59985 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -1,3 +1,5 @@ +require 'active_support/per_thread_registry' + module ActiveRecord module Scoping extend ActiveSupport::Concern @@ -9,11 +11,11 @@ module ActiveRecord module ClassMethods def current_scope #:nodoc: - Thread.current["#{self}_current_scope"] + ScopeRegistry.value_for(:current_scope, base_class.to_s) end def current_scope=(scope) #:nodoc: - Thread.current["#{self}_current_scope"] = scope + ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope) end end @@ -24,5 +26,57 @@ module ActiveRecord send("#{att}=", value) if respond_to?("#{att}=") end end + + # This class stores the +:current_scope+ and +:ignore_default_scope+ values + # for different classes. The registry is stored as a thread local, which is + # accessed through +ScopeRegistry.current+. + # + # This class allows you to store and get the scope values on different + # classes and different types of scopes. For example, if you are attempting + # to get the current_scope for the +Board+ model, then you would use the + # following code: + # + # registry = ActiveRecord::Scoping::ScopeRegistry + # registry.set_value_for(:current_scope, "Board", some_new_scope) + # + # Now when you run: + # + # registry.value_for(:current_scope, "Board") + # + # You will obtain whatever was defined in +some_new_scope+. The +value_for+ + # and +set_value_for+ methods are delegated to the current +ScopeRegistry+ + # object, so the above example code can also be called as: + # + # ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope, + # "Board", some_new_scope) + class ScopeRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + VALID_SCOPE_TYPES = [:current_scope, :ignore_default_scope] + + def initialize + @registry = Hash.new { |hash, key| hash[key] = {} } + end + + # Obtains the value for a given +scope_name+ and +variable_name+. + def value_for(scope_type, variable_name) + raise_invalid_scope_type!(scope_type) + @registry[scope_type][variable_name] + end + + # Sets the +value+ for a given +scope_type+ and +variable_name+. + def set_value_for(scope_type, variable_name, value) + raise_invalid_scope_type!(scope_type) + @registry[scope_type][variable_name] = value + end + + private + + def raise_invalid_scope_type!(scope_type) + if !VALID_SCOPE_TYPES.include?(scope_type) + raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES" + end + end + end end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index cde4f29d08..d37d33d552 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -5,8 +5,17 @@ module ActiveRecord included do # Stores the default scope for the class. - class_attribute :default_scopes, instance_writer: false + class_attribute :default_scopes, instance_writer: false, instance_predicate: false + self.default_scopes = [] + + def self.default_scopes? + ActiveSupport::Deprecation.warn( + "#default_scopes? is deprecated. Do something like #default_scopes.empty? instead." + ) + + !!self.default_scopes + end end module ClassMethods @@ -111,11 +120,11 @@ module ActiveRecord end def ignore_default_scope? # :nodoc: - Thread.current["#{self}_ignore_default_scope"] + ScopeRegistry.value_for(:ignore_default_scope, self) end def ignore_default_scope=(ignore) # :nodoc: - Thread.current["#{self}_ignore_default_scope"] = ignore + ScopeRegistry.set_value_for(:ignore_default_scope, self, ignore) end # The ignore_default_scope flag is used to prevent an infinite recursion diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 12317601b6..da73bead32 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -160,13 +160,8 @@ module ActiveRecord singleton_class.send(:define_method, name) do |*args| if body.respond_to?(:call) - scope = extension ? body.call(*args).extending(extension) : body.call(*args) - - if scope - default_scoped = scope.default_scoped - scope = relation.merge(scope) - scope.default_scoped = default_scoped - end + scope = all.scoping { body.call(*args) } + scope = scope.extending(extension) if extension else scope = body end diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb new file mode 100644 index 0000000000..dd4ee0c4a0 --- /dev/null +++ b/activerecord/lib/active_record/statement_cache.rb @@ -0,0 +1,26 @@ +module ActiveRecord + + # Statement cache is used to cache a single statement in order to avoid creating the AST again. + # Initializing the cache is done by passing the statement in the initialization block: + # + # cache = ActiveRecord::StatementCache.new do + # Book.where(name: "my book").limit(100) + # end + # + # The cached statement is executed by using the +execute+ method: + # + # cache.execute + # + # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped. + # Database is queried when +to_a+ is called on the relation. + class StatementCache + def initialize + @relation = yield + raise ArgumentError.new("Statement cannot be nil") if @relation.nil? + end + + def execute + @relation.dup.to_a + end + end +end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 4dbd668fcf..a5955ccba4 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -339,8 +339,12 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc: @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) - @_start_transaction_state[:new_record] = @new_record - @_start_transaction_state[:destroyed] = @destroyed + unless @_start_transaction_state.include?(:new_record) + @_start_transaction_state[:new_record] = @new_record + end + unless @_start_transaction_state.include?(:destroyed) + @_start_transaction_state[:destroyed] = @destroyed + end @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 @_start_transaction_state[:frozen?] = @attributes.frozen? end |