diff options
Diffstat (limited to 'activerecord')
62 files changed, 802 insertions, 190 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index c6ce77a9cc..52300fd53f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,13 +1,117 @@ ## Rails 4.0.0 (unreleased) ## +* `0x` prefix must be added when assigning hexadecimal string into `bit` column in PostgreSQL. + + *kennyj* + +* Added Statement Cache to allow the caching of a single statement. The cache works by + duping the relation returned from yielding a statement, which allows skipping the AST + building phase for following executes. The cache returns results in array format. + + Example: + + cache = ActiveRecord::StatementCache.new do + Book.where(name: "my book").limit(100) + end + + books = cache.execute + + The solution attempts to get closer to the speed of `find_by_sql` but still maintaining + the expressiveness of the Active Record queries. + + *Olli Rissanen* + +* Preserve context while merging relations with join information. + + class Comment < ActiveRecord::Base + belongs_to :post + end + + class Author < ActiveRecord::Base + has_many :posts + end + + class Post < ActiveRecord::Base + belongs_to :author + has_many :comments + end + + `Comment.joins(:post).merge(Post.joins(:author).merge(Author.where(:name => "Joe Blogs"))).all` + would fail with + `ActiveRecord::ConfigurationError: Association named 'author' was not found on Comment`. + + It is failing because `all` is being called on relation which looks like this after all + the merging: `{:joins=>[:post, :author], :where=>[#<Arel::Nodes::Equality: ....}`. In this + relation all the context that `Post` was joined with `Author` is lost and hence the error + that `author` was not found on `Comment`. + + The solution is to build `JoinAssociation` when two relations with join information are being + merged. And later while building the Arel use the previously built `JoinAssociation` record + in `JoinDependency#graft` to build the right from clause. + Fixes #3002. + + *Jared Armstrong and Neeraj Singh* + +* `default_scopes?` is deprecated. Check for `default_scopes.empty?` instead. + + *Agis Anastasopoulos* + +* Default values for PostgreSQL bigint types now get parsed and dumped to the + schema correctly. + + *Erik Peterson* + +* Fix associations with `:inverse_of` option when building association + with a block. Inside the block the parent object was different then + after the block. + + Example: + + parent.association.build do |child| + child.parent.equal?(parent) # false + end + + # vs + + child = parent.association.build + child.parent.equal?(parent) # true + + *Michal Cichra* + +* `has_many` using `:through` now obeys the order clause mentioned in + through association. + Fixes #10016. + + *Neeraj Singh* + +* `belongs_to :touch` behavior now touches old association when + transitioning to new association. + + class Passenger < ActiveRecord::Base + belongs_to :car, touch: true + end + + car_1 = Car.create + car_2 = Car.create + + passenger = Passenger.create car: car_1 + + passenger.car = car_2 + passenger.save + + Previously only car_2 would be touched. Now both car_1 and car_2 + will be touched. + + *Adam Gamble* + * Extract and deprecate Firebird / Sqlserver / Oracle database tasks, because These tasks should be supported by 3rd-party adapter. *kennyj* * Allow `ActiveRecord::Base.connection_handler` to have thread affinity and be - settable, this effectively allows Active Record to be used in a multi threaded - setup with multiple connections to multiple dbs. + settable, this effectively allows Active Record to be used in a multithreaded + setup with multiple connections to multiple databases. *Sam Saffron* @@ -41,7 +145,8 @@ *Ken Mazaika* * Add an `add_index` override in PostgreSQL adapter and MySQL adapter - to allow custom index type support. Fixes #6101. + to allow custom index type support. + Fixes #6101. add_index(:wikis, :body, :using => 'gin') @@ -65,7 +170,6 @@ in the association for a particular id. Then, it will go to the DB if it is not found. This is accomplished by calling `find_by_scan` in collection associations whenever `options[:inverse_of]` is not nil. - Fixes #9470. *John Wang* @@ -1175,7 +1279,7 @@ * Explain only normal CRUD sql (select / update / insert / delete). Fix problem that explains unexplainable sql. - Closes #7544 #6458. + Fixes #7544 #6458. *kennyj* @@ -1804,13 +1908,13 @@ add_index(:accounts, :code, where: 'active') - Generates + generates CREATE INDEX index_accounts_on_code ON accounts(code) WHERE active *Marcelo Silveira* -* Implemented ActiveRecord::Relation#none method. +* Implemented `ActiveRecord::Relation#none` method. The `none` method returns a chainable relation with zero records (an instance of the NullRelation class). diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 3e3475f709..a86c512863 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -25,5 +25,5 @@ Gem::Specification.new do |s| s.add_dependency 'activemodel', version s.add_dependency 'arel', '~> 4.0.0.beta2' - s.add_dependency 'activerecord-deprecated_finders', '~> 0.0.3' + s.add_dependency 'activerecord-deprecated_finders', '~> 1.0.1' end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index f08cca651f..0330c0f37f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -35,8 +35,8 @@ module ActiveRecord autoload :Base autoload :Callbacks autoload :Core - autoload :CounterCache autoload :ConnectionHandling + autoload :CounterCache autoload :DynamicMatchers autoload :Explain autoload :Inheritance @@ -50,12 +50,14 @@ module ActiveRecord autoload :Querying autoload :ReadonlyAttributes autoload :Reflection + autoload :RuntimeRegistry autoload :Sanitization autoload :Schema autoload :SchemaDumper autoload :SchemaMigration autoload :Scoping autoload :Serialization + autoload :StatementCache autoload :Store autoload :Timestamp autoload :Transactions @@ -69,8 +71,8 @@ module ActiveRecord autoload :Aggregations autoload :Associations - autoload :AttributeMethods autoload :AttributeAssignment + autoload :AttributeMethods autoload :AutosaveAssociation autoload :Relation 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 diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb new file mode 100644 index 0000000000..d7d77f96e2 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -0,0 +1,87 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlByteaTest < ActiveRecord::TestCase + class ByteaDataType < ActiveRecord::Base + self.table_name = 'bytea_data_type' + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.transaction do + @connection.create_table('bytea_data_type') do |t| + t.binary 'payload' + end + end + end + @column = ByteaDataType.columns.find { |c| c.name == 'payload' } + assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn)) + end + + def teardown + @connection.execute 'drop table if exists bytea_data_type' + end + + def test_column + assert_equal :binary, @column.type + end + + def test_type_cast_binary_converts_the_encoding + assert @column + + data = "\u001F\x8B" + assert_equal('UTF-8', data.encoding.name) + assert_equal('ASCII-8BIT', @column.type_cast(data).encoding.name) + end + + def test_type_cast_binary_value + data = "\u001F\x8B".force_encoding("BINARY") + assert_equal(data, @column.type_cast(data)) + end + + def test_type_case_nil + assert_equal(nil, @column.type_cast(nil)) + end + + def test_read_value + data = "\u001F" + @connection.execute "insert into bytea_data_type (payload) VALUES ('#{data}')" + record = ByteaDataType.first + assert_equal(data, record.payload) + record.delete + end + + def test_read_nil_value + @connection.execute "insert into bytea_data_type (payload) VALUES (null)" + record = ByteaDataType.first + assert_equal(nil, record.payload) + record.delete + end + + def test_write_value + data = "\u001F" + record = ByteaDataType.create(payload: data) + refute record.new_record? + assert_equal(data, record.payload) + end + + def test_write_binary + data = File.read(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'example.log')) + assert(data.size > 1) + record = ByteaDataType.create(payload: data) + refute record.new_record? + assert_equal(data, record.payload) + assert_equal(data, ByteaDataType.where(id: record.id).first.payload) + end + + def test_write_nil + record = ByteaDataType.create(payload: nil) + refute record.new_record? + assert_equal(nil, record.payload) + assert_equal(nil, ByteaDataType.where(id: record.id).first.payload) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 1e6ae85a25..8c17372286 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -545,13 +545,19 @@ _SQL def test_update_bit_string new_bit_string = '11111111' - new_bit_string_varying = '11111110' + new_bit_string_varying = '0xFF' assert @first_bit_string.bit_string = new_bit_string assert @first_bit_string.bit_string_varying = new_bit_string_varying assert @first_bit_string.save assert @first_bit_string.reload - assert_equal new_bit_string, @first_bit_string.bit_string - assert_equal new_bit_string_varying, @first_bit_string.bit_string_varying + assert_equal @first_bit_string.bit_string, new_bit_string + assert_equal @first_bit_string.bit_string, @first_bit_string.bit_string_varying + end + + def test_invalid_hex_string + new_bit_string = 'FF' + @first_bit_string.bit_string = new_bit_string + assert_raise(ActiveRecord::StatementInvalid) { assert @first_bit_string.save } end def test_update_oid diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 619d581d5f..0b61f61572 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -22,13 +22,6 @@ module ActiveRecord assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain assert_match %(Seq Scan on audit_logs), explain end - - def test_dont_explain_for_set_search_path - queries = Thread.current[:available_queries_for_explain] = [] - ActiveRecord::Base.connection.schema_search_path = "public" - assert queries.empty? - end - end end end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index a5a22bc30b..e78cb88562 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -54,7 +54,7 @@ class CopyTableTest < ActiveRecord::TestCase end def test_copy_table_with_id_col_that_is_not_primary_key - test_copy_table('goofy_string_id', 'goofy_string_id2') do |from, to, options| + test_copy_table('goofy_string_id', 'goofy_string_id2') do original_id = @connection.columns('goofy_string_id').detect{|col| col.name == 'id' } copied_id = @connection.columns('goofy_string_id2').detect{|col| col.name == 'id' } assert_equal original_id.type, copied_id.type @@ -65,7 +65,7 @@ class CopyTableTest < ActiveRecord::TestCase end def test_copy_table_with_unconventional_primary_key - test_copy_table('owners', 'owners_unconventional') do |from, to, options| + test_copy_table('owners', 'owners_unconventional') do original_pk = @connection.primary_key('owners') copied_pk = @connection.primary_key('owners_unconventional') assert_equal original_pk, copied_pk diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index 10195e3ae4..5536702f58 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -141,7 +141,6 @@ class AggregationsTest < ActiveRecord::TestCase end class OverridingAggregationsTest < ActiveRecord::TestCase - class Name; end class DifferentName; end class Person < ActiveRecord::Base diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index d6850215b5..4aa6567d85 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -302,7 +302,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_foreign_keys pets = Pet.all.merge!(:includes => :owner).to_a - assert_equal 3, pets.length + assert_equal 4, pets.length end def test_eager_association_loading_with_belongs_to diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 67d18f313a..70c6b489aa 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -583,7 +583,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys - assert_equal 1, owners(:blackbeard).toys.count + assert_equal 2, owners(:blackbeard).toys.count end def test_find_on_has_many_association_collection_with_include_and_conditions @@ -882,6 +882,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [tags(:general)], post.reload.tags end + def test_has_many_through_obeys_order_on_through_association + owner = owners(:blackbeard) + assert owner.toys.to_sql.include?("pets.name desc") + assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name } + end + test "has many through associations on new records use null relations" do person = Person.new diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 85d0ad0aa1..ec128acf28 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -235,6 +235,22 @@ class InverseHasManyTests < ActiveRecord::TestCase assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" end + def test_parent_instance_should_be_shared_within_create_block_of_new_child + man = Man.first + interest = man.interests.build do |i| + assert i.man.equal?(man), "Man of child should be the same instance as a parent" + end + assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent" + end + + def test_parent_instance_should_be_shared_within_build_block_of_new_child + man = Man.first + interest = man.interests.build do |i| + assert i.man.equal?(man), "Man of child should be the same instance as a parent" + end + assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent" + end + def test_parent_instance_should_be_shared_with_poked_in_child m = men(:gordon) i = Interest.create(:topic => 'Industrial Revolution Re-enactment') diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index a06bacafca..95c571fd03 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -245,7 +245,6 @@ class AssociationProxyTest < ActiveRecord::TestCase end class OverridingAssociationsTest < ActiveRecord::TestCase - class Person < ActiveRecord::Base; end class DifferentPerson < ActiveRecord::Base; end class PeopleList < ActiveRecord::Base diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 648596b828..387c741762 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -69,7 +69,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_boolean_attributes - assert ! Topic.find(1).approved? + assert !Topic.find(1).approved? assert Topic.find(2).approved? end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 08223902c7..bd568af06a 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -21,7 +21,6 @@ require 'models/parrot' require 'models/person' require 'models/edge' require 'models/joke' -require 'models/bulb' require 'models/bird' require 'models/car' require 'models/bulb' @@ -1378,9 +1377,9 @@ class BasicsTest < ActiveRecord::TestCase UnloadablePost.send(:current_scope=, UnloadablePost.all) UnloadablePost.unloadable - assert_not_nil Thread.current[:UnloadablePost_current_scope] + assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost") ActiveSupport::Dependencies.remove_unloadable_constants! - assert_nil Thread.current[:UnloadablePost_current_scope] + assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost") ensure Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost) end diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index b425967678..fb53a92c89 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -1,55 +1,54 @@ require 'cases/helper' +require 'active_record/explain_subscriber' +require 'active_record/explain_registry' if ActiveRecord::Base.connection.supports_explain? class ExplainSubscriberTest < ActiveRecord::TestCase SUBSCRIBER = ActiveRecord::ExplainSubscriber.new - def test_collects_nothing_if_available_queries_for_explain_is_nil - with_queries(nil) do - SUBSCRIBER.finish(nil, nil, {}) - assert_nil Thread.current[:available_queries_for_explain] - end + def setup + ActiveRecord::ExplainRegistry.reset + ActiveRecord::ExplainRegistry.collect = true end def test_collects_nothing_if_the_payload_has_an_exception - with_queries([]) do |queries| - SUBSCRIBER.finish(nil, nil, :exception => Exception.new) - assert queries.empty? - end + SUBSCRIBER.finish(nil, nil, exception: Exception.new) + assert queries.empty? end def test_collects_nothing_for_ignored_payloads - with_queries([]) do |queries| - ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip| - SUBSCRIBER.finish(nil, nil, :name => ip) - end - assert queries.empty? + ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip| + SUBSCRIBER.finish(nil, nil, name: ip) end + assert queries.empty? + end + + def test_collects_nothing_if_collect_is_false + ActiveRecord::ExplainRegistry.collect = false + SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'select 1 from users', binds: [1, 2]) + assert queries.empty? end def test_collects_pairs_of_queries_and_binds sql = 'select 1 from users' binds = [1, 2] - with_queries([]) do |queries| - SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => sql, :binds => binds) - assert_equal 1, queries.size - assert_equal sql, queries[0][0] - assert_equal binds, queries[0][1] - end + SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: sql, binds: binds) + assert_equal 1, queries.size + assert_equal sql, queries[0][0] + assert_equal binds, queries[0][1] end - def test_collects_nothing_if_unexplained_sqls - with_queries([]) do |queries| - SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => 'SHOW max_identifier_length') - assert queries.empty? - end + def test_collects_nothing_if_the_statement_is_not_whitelisted + SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'SHOW max_identifier_length') + assert queries.empty? + end + + def teardown + ActiveRecord::ExplainRegistry.reset end - def with_queries(queries) - Thread.current[:available_queries_for_explain] = queries - yield queries - ensure - Thread.current[:available_queries_for_explain] = nil + def queries + ActiveRecord::ExplainRegistry.queries end end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 572431ee87..db3bb56f1f 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -12,13 +12,13 @@ require 'models/minimalistic' require 'models/warehouse_thing' require 'models/parrot' require 'models/minivan' +require 'models/owner' require 'models/person' require 'models/pet' require 'models/toy' require 'rexml/document' class PersistencesTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys # Oracle UPDATE does not support ORDER BY diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 9ca980fdf6..34ecdb3cc9 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -1,10 +1,12 @@ require "cases/helper" require 'models/post' require 'models/comment' +require 'models/author' +require 'models/rating' module ActiveRecord class RelationTest < ActiveRecord::TestCase - fixtures :posts, :comments + fixtures :posts, :comments, :authors class FakeKlass < Struct.new(:table_name, :name) end @@ -176,6 +178,13 @@ module ActiveRecord relation.merge!(where: ['foo = ?', 'bar']) assert_equal ['foo = bar'], relation.where_values end + + def test_relation_merging_with_merged_joins + special_comments_with_ratings = SpecialComment.joins(:ratings) + posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) + assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length + end + end class RelationMutationTest < ActiveSupport::TestCase diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 9944527d48..a48ae1036f 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -242,6 +242,11 @@ class SchemaDumperTest < ActiveRecord::TestCase end if current_adapter?(:PostgreSQLAdapter) + def test_schema_dump_includes_bigint_default + output = standard_dump + assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output + end + def test_schema_dump_includes_extensions connection = ActiveRecord::Base.connection skip unless connection.supports_extensions? diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 3e2e6ab701..afe32af1d1 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -459,4 +459,9 @@ class NamedScopingTest < ActiveRecord::TestCase end assert_equal [posts(:welcome).title], klass.all.map(&:title) end + + def test_subclass_merges_scopes_properly + assert_equal 1, SpecialComment.where(body: 'go crazy').created.count + end + end diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb new file mode 100644 index 0000000000..76da49707f --- /dev/null +++ b/activerecord/test/cases/statement_cache_test.rb @@ -0,0 +1,64 @@ +require 'cases/helper' +require 'models/book' +require 'models/liquid' +require 'models/molecule' +require 'models/electron' + +module ActiveRecord + class StatementCacheTest < ActiveRecord::TestCase + def setup + @connection = ActiveRecord::Base.connection + end + + def test_statement_cache_with_simple_statement + cache = ActiveRecord::StatementCache.new do + Book.where(name: "my book").where("author_id > 3") + end + + Book.create(name: "my book", author_id: 4) + + books = cache.execute + assert_equal "my book", books[0].name + end + + def test_statement_cache_with_nil_statement_raises_error + assert_raise(ArgumentError) do + ActiveRecord::StatementCache.new do + nil + end + end + end + + def test_statement_cache_with_complex_statement + cache = ActiveRecord::StatementCache.new do + Liquid.joins(:molecules => :electrons).where('molecules.name' => 'dioxane', 'electrons.name' => 'lepton') + end + + salty = Liquid.create(name: 'salty') + molecule = salty.molecules.create(name: 'dioxane') + molecule.electrons.create(name: 'lepton') + + liquids = cache.execute + assert_equal "salty", liquids[0].name + end + + def test_statement_cache_values_differ + cache = ActiveRecord::StatementCache.new do + Book.where(name: "my book") + end + + 3.times do + Book.create(name: "my book") + end + + first_books = cache.execute + + 3.times do + Book.create(name: "my book") + end + + additional_books = cache.execute + assert first_books != additional_books + end + end +end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 777a2b70dd..9d84f64c96 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -176,6 +176,52 @@ class TimestampTest < ActiveRecord::TestCase assert_not_equal time, owner.updated_at end + def test_changing_parent_of_a_record_touches_both_new_and_old_parent_record + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Toy'; end + belongs_to :pet, touch: true + end + + toy1 = klass.find(1) + old_pet = toy1.pet + + toy2 = klass.find(2) + new_pet = toy2.pet + time = 3.days.ago.at_beginning_of_hour + + old_pet.update_columns(updated_at: time) + new_pet.update_columns(updated_at: time) + + toy1.pet = new_pet + toy1.save! + + old_pet.reload + new_pet.reload + + assert_not_equal time, new_pet.updated_at + assert_not_equal time, old_pet.updated_at + end + + def test_clearing_association_touches_the_old_record + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Toy'; end + belongs_to :pet, touch: true + end + + toy = klass.find(1) + pet = toy.pet + time = 3.days.ago.at_beginning_of_hour + + pet.update_columns(updated_at: time) + + toy.pet = nil + toy.save! + + pet.reload + + assert_not_equal time, pet.updated_at + end + def test_timestamp_attributes_for_create toy = Toy.first assert_equal toy.send(:timestamp_attributes_for_create), [:created_at, :created_on] diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 766a5c0c90..9485de88a6 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -281,38 +281,6 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end end - -class SaveFromAfterCommitBlockTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false - - class TopicWithSaveInCallback < ActiveRecord::Base - self.table_name = :topics - after_commit :cache_topic, :on => :create - after_commit :call_update, :on => :update - attr_accessor :cached, :record_updated - - def call_update - self.record_updated = true - end - - def cache_topic - unless cached - self.cached = true - self.save - else - self.cached = false - end - end - end - - def test_after_commit_in_save - topic = TopicWithSaveInCallback.new() - topic.save - assert_equal true, topic.cached - assert_equal true, topic.record_updated - end -end - class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase self.use_transactional_fixtures = false diff --git a/activerecord/test/fixtures/pets.yml b/activerecord/test/fixtures/pets.yml index a1601a53f0..2ec4f53e6d 100644 --- a/activerecord/test/fixtures/pets.yml +++ b/activerecord/test/fixtures/pets.yml @@ -12,3 +12,8 @@ mochi: pet_id: 3 name: mochi owner_id: 2 + +bulbul: + pet_id: 4 + name: bulbul + owner_id: 1 diff --git a/activerecord/test/fixtures/toys.yml b/activerecord/test/fixtures/toys.yml index 037e335e0a..ae9044ec62 100644 --- a/activerecord/test/fixtures/toys.yml +++ b/activerecord/test/fixtures/toys.yml @@ -2,3 +2,13 @@ bone: toy_id: 1 name: Bone pet_id: 1 + +doll: + toy_id: 2 + name: Doll + pet_id: 2 + +bulbuli: + toy_id: 3 + name: Bulbuli + pet_id: 4 diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index fea55f4535..1c7ed4aa3e 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -1,5 +1,5 @@ class Owner < ActiveRecord::Base self.primary_key = :owner_id - has_many :pets + has_many :pets, -> { order 'pets.name desc' } has_many :toys, :through => :pets end diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb index 3cd5bceed5..f7970d7aab 100644 --- a/activerecord/test/models/pet.rb +++ b/activerecord/test/models/pet.rb @@ -1,5 +1,4 @@ class Pet < ActiveRecord::Base - attr_accessor :current_user self.primary_key = :pet_id @@ -13,5 +12,4 @@ class Pet < ActiveRecord::Base after_destroy do |record| Pet.after_destroy_output = record.current_user end - end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index d8271ac8d1..6b7012a172 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -32,6 +32,7 @@ ActiveRecord::Schema.define do char3 text default 'a text field', positive_integer integer default 1, negative_integer integer default -1, + bigint_default bigint default 0::bigint, decimal_number decimal(3,2) default 2.78, multiline_default text DEFAULT '--- [] |