diff options
Diffstat (limited to 'activerecord/lib/active_record')
203 files changed, 6406 insertions, 3623 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index e576ec4d40..39077aea7e 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -230,8 +230,8 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) define_method(name) do - if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !read_attribute(key).nil? }) - attrs = mapping.collect {|key, _| read_attribute(key)} + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !_read_attribute(key).nil? }) + attrs = mapping.collect {|key, _| _read_attribute(key)} object = constructor.respond_to?(:call) ? constructor.call(*attrs) : class_name.constantize.send(constructor, *attrs) @@ -245,7 +245,8 @@ module ActiveRecord define_method("#{name}=") do |part| klass = class_name.constantize if part.is_a?(Hash) - part = klass.new(*part.values) + raise ArgumentError unless part.size == part.keys.max + part = klass.new(*part.sort.map(&:last)) end unless part.is_a?(klass) || converter.nil? || part.nil? diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 5a84792f45..f2b44913db 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -1,7 +1,7 @@ module ActiveRecord class AssociationRelation < Relation - def initialize(klass, table, association) - super(klass, table) + def initialize(klass, table, predicate_builder, association) + super(klass, table, predicate_builder) @association = association end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6222bfe903..a146d78a5a 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -46,12 +46,18 @@ module ActiveRecord end end + class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc: + def initialize(owner_class_name, reflection) + super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + end + end + class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc: def initialize(reflection) through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names source_associations = reflection.through_reflection.klass._reflections.keys - super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") + super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") end end @@ -110,6 +116,7 @@ module ActiveRecord autoload :Association, 'active_record/associations/association' autoload :SingularAssociation, 'active_record/associations/singular_association' autoload :CollectionAssociation, 'active_record/associations/collection_association' + autoload :ForeignAssociation, 'active_record/associations/foreign_association' autoload :CollectionProxy, 'active_record/associations/collection_proxy' autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' @@ -447,9 +454,11 @@ module ActiveRecord # # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+. # - # Should any of the +before_add+ callbacks throw an exception, the object does not get - # added to the collection. Same with the +before_remove+ callbacks; if an exception is - # thrown the object doesn't get removed. + # If any of the +before_add+ callbacks throw an exception, the object will not be + # added to the collection. + # + # Similarly, if any of the +before_remove+ callbacks throw an exception, the object + # will not be removed from the collection. # # == Association extensions # @@ -647,7 +656,7 @@ module ActiveRecord # belongs_to :commenter # end # - # When using nested association, you will not be able to modify the association because there + # When using a nested association, you will not be able to modify the association because there # is not enough information to know what modification to make. For example, if you tried to # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. @@ -717,9 +726,9 @@ module ActiveRecord # == Eager loading of associations # # Eager loading is a way to find objects of a certain class and a number of named associations. - # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 + # It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100 # posts that each need to display their author triggers 101 database queries. Through the - # use of eager loading, the 101 queries can be reduced to 2. + # use of eager loading, the number of queries will be reduced from 101 to 2. # # class Post < ActiveRecord::Base # belongs_to :author @@ -749,16 +758,16 @@ module ActiveRecord # Post.includes(:author, :comments).each do |post| # # This will load all comments with a single query. This reduces the total number of queries - # to 3. More generally the number of queries will be 1 plus the number of associations + # to 3. In general, the number of queries will be 1 plus the number of associations # named (except if some of the associations are polymorphic +belongs_to+ - see below). # # To include a deep hierarchy of associations, use a hash: # - # Post.includes(:author, {comments: {author: :gravatar}}).each do |post| + # Post.includes(:author, { comments: { author: :gravatar } }).each do |post| # - # That'll grab not only all the comments but all their authors and gravatar pictures. - # You can mix and match symbols, arrays and hashes in any combination to describe the - # associations you want to load. + # The above code will load all the comments and all of their associated + # authors and gravatars. You can mix and match any combination of symbols, + # arrays, and hashes to retrieve the associations you want to load. # # All of this power shouldn't fool you into thinking that you can pull out huge amounts # of data with no performance penalty just because you've reduced the number of queries. @@ -767,8 +776,8 @@ module ActiveRecord # cut down on the number of queries in a situation as the one described above. # # Since only one table is loaded at a time, conditions or orders cannot reference tables - # other than the main one. If this is the case Active Record falls back to the previously - # used LEFT OUTER JOIN based strategy. For example + # other than the main one. If this is the case, Active Record falls back to the previously + # used LEFT OUTER JOIN based strategy. For example: # # Post.includes([:author, :comments]).where(['comments.approved = ?', true]) # @@ -991,6 +1000,8 @@ module ActiveRecord # callbacks declared either before or after the <tt>:dependent</tt> option # can affect what it does. # + # Note that <tt>:dependent</tt> option is ignored for +has_one+ <tt>:through</tt> associations. + # # === Delete or destroy? # # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>, @@ -1003,7 +1014,7 @@ module ActiveRecord # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. - # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for + # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete # the join records, without running their callbacks). # @@ -1052,7 +1063,7 @@ module ActiveRecord # Specifies a one-to-many association. The following methods for retrieval and query of # collections of associated objects will be added: # - # +collection+ is a placeholder for the symbol passed as the first argument, so + # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. # # [collection(force_reload = false)] @@ -1131,7 +1142,32 @@ module ActiveRecord # * <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. + # The declaration can also include an +options+ hash to specialize the behavior of the association. + # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific set of records or customize the generated + # query when you access the associated collection. + # + # Scope examples: + # has_many :comments, -> { where(author_id: 1) } + # has_many :employees, -> { joins(:address) } + # has_many :posts, ->(post) { where("max_post_length > ?", post.length) } + # + # === Extensions + # + # The +extension+ argument allows you to pass a block into a has_many + # association. This is useful for adding new finders, creators and other + # factory-type methods to be used as part of the association. + # + # Extension examples: + # has_many :employees do + # def find_or_create_by_name(name) + # first_name, last_name = name.split(" ", 2) + # find_or_create_by(first_name: first_name, last_name: last_name) + # end + # end # # === Options # [:class_name] @@ -1143,8 +1179,14 @@ module ActiveRecord # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ # association will use "person_id" as the default <tt>:foreign_key</tt>. + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the polymorphic association + # specified on "as" option with a "_type" suffix. So a class that defines a + # <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the + # default <tt>:foreign_type</tt>. # [:primary_key] - # Specify the method that returns the primary key used for the association. By default this is +id+. + # Specify the name of the column to use as the primary key for the association. By default this is +id+. # [:dependent] # Controls what happens to the associated objects when # their owner is destroyed. Note that these are implemented as @@ -1205,11 +1247,15 @@ module ActiveRecord # that is the inverse of this <tt>has_many</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:extend] + # Specifies a module or array of modules that will be extended into the association object returned. + # Useful for defining methods on associations, especially when they should be shared between multiple + # association objects. # # Option examples: # has_many :comments, -> { order "posted_on" } # has_many :comments, -> { includes :author } - # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person" + # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person" # has_many :tracks, -> { order "position" }, dependent: :destroy # has_many :comments, dependent: :nullify # has_many :tags, as: :taggable @@ -1227,7 +1273,7 @@ module ActiveRecord # # The following methods for retrieval and query of a single associated object will be added: # - # +association+ is a placeholder for the symbol passed as the first argument, so + # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>. # # [association(force_reload = false)] @@ -1257,9 +1303,20 @@ module ActiveRecord # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>) # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>) # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific record or customize the generated query + # when you access the associated object. + # + # Scope examples: + # has_one :author, -> { where(comment_id: 1) } + # has_one :employer, -> { joins(:company) } + # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) } + # # === Options # - # The declaration can also include an options hash to specialize the behavior of the association. + # The declaration can also include an +options+ hash to specialize the behavior of the association. # # Options are: # [:class_name] @@ -1275,10 +1332,18 @@ module ActiveRecord # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object + # + # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association # will use "person_id" as the default <tt>:foreign_key</tt>. + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the polymorphic association + # specified on "as" option with a "_type" suffix. So a class that defines a + # <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the + # default <tt>:foreign_type</tt>. # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. # [:as] @@ -1309,6 +1374,10 @@ module ActiveRecord # that is the inverse of this <tt>has_one</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # has_one :credit_card, dependent: :destroy # destroys the associated credit card @@ -1317,9 +1386,10 @@ module ActiveRecord # has_one :last_comment, -> { order 'posted_on' }, class_name: "Comment" # has_one :project_manager, -> { where role: 'project_manager' }, class_name: "Person" # has_one :attachment, as: :attachable - # has_one :boss, readonly: :true + # has_one :boss, -> { readonly } # has_one :club, through: :membership # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable + # has_one :credit_card, required: true def has_one(name, scope = nil, options = {}) reflection = Builder::HasOne.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1333,7 +1403,7 @@ module ActiveRecord # Methods will be added for retrieval and query for a single associated object, for which # this object holds an id: # - # +association+ is a placeholder for the symbol passed as the first argument, so + # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>. # # [association(force_reload = false)] @@ -1359,7 +1429,18 @@ module ActiveRecord # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>) - # The declaration can also include an options hash to specialize the behavior of the association. + # The declaration can also include an +options+ hash to specialize the behavior of the association. + # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific record or customize the generated query + # when you access the associated object. + # + # Scope examples: + # belongs_to :firm, -> { where(id: 2) } + # belongs_to :user, -> { joins(:friends) } + # belongs_to :level, ->(level) { where("game_level > ?", level.current) } # # === Options # @@ -1413,7 +1494,7 @@ module ActiveRecord # # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [:touch] - # If true, the associated object will be touched (the updated_at/on attributes set to now) + # If true, the associated object will be touched (the updated_at/on attributes set to current time) # when this record is either saved or destroyed. If you specify a symbol, that attribute # will be updated with the current time in addition to the updated_at/on attribute. # [:inverse_of] @@ -1421,18 +1502,23 @@ module ActiveRecord # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # belongs_to :firm, foreign_key: "client_of" # belongs_to :person, primary_key: "name", foreign_key: "person_name" # belongs_to :author, class_name: "Person", foreign_key: "author_id" - # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" }, + # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }, # class_name: "Coupon", foreign_key: "coupon_id" # belongs_to :attachable, polymorphic: true - # belongs_to :project, readonly: true + # belongs_to :project, -> { readonly } # belongs_to :post, counter_cache: true - # belongs_to :company, touch: true + # belongs_to :comment, touch: true # belongs_to :company, touch: :employees_last_updated_at + # belongs_to :user, required: true def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1470,7 +1556,7 @@ module ActiveRecord # # Adds the following methods for retrieval and query: # - # +collection+ is a placeholder for the symbol passed as the first argument, so + # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. # # [collection(force_reload = false)] @@ -1531,7 +1617,34 @@ module ActiveRecord # * <tt>Developer#projects.exists?(...)</tt> # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>) # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>) - # The declaration may include an options hash to specialize the behavior of the association. + # The declaration may include an +options+ hash to specialize the behavior of the association. + # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific set of records or customize the generated + # query when you access the associated collection. + # + # Scope examples: + # has_and_belongs_to_many :projects, -> { includes :milestones, :manager } + # has_and_belongs_to_many :categories, ->(category) { + # where("default_category = ?", category.name) + # } + # + # === Extensions + # + # The +extension+ argument allows you to pass a block into a + # has_and_belongs_to_many association. This is useful for adding new + # finders, creators and other factory-type methods to be used as part of + # the association. + # + # Extension examples: + # has_and_belongs_to_many :contractors do + # def find_or_create_by_name(name) + # first_name, last_name = name.split(" ", 2) + # find_or_create_by(first_name: first_name, last_name: last_name) + # end + # end # # === Options # @@ -1577,7 +1690,7 @@ module ActiveRecord scope = nil end - habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(:has_and_belongs_to_many, name, scope, options, self) + habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) builder = Builder::HasAndBelongsToMany.new name, self, options @@ -1608,7 +1721,7 @@ module ActiveRecord hm_options[:through] = middle_reflection.name hm_options[:source] = join_model.right_reflection.name - [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table].each do |k| + [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name].each do |k| hm_options[k] = options[k] if options.key? k end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index a6a1947148..2b7e4f28c5 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -5,20 +5,23 @@ module ActiveRecord # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and # ActiveRecord::Associations::ThroughAssociationScope class AliasTracker # :nodoc: - attr_reader :aliases, :connection + attr_reader :aliases - def self.empty(connection) - new connection, Hash.new(0) + def self.create(connection, initial_table, type_caster) + aliases = Hash.new(0) + aliases[initial_table] = 1 + new connection, aliases, type_caster end - def self.create(connection, table_joins) - if table_joins.empty? - empty connection + def self.create_with_joins(connection, initial_table, joins, type_caster) + if joins.empty? + create(connection, initial_table, type_caster) else - aliases = Hash.new { |h,k| - h[k] = initial_count_for(connection, k, table_joins) + aliases = Hash.new { |h, k| + h[k] = initial_count_for(connection, k, joins) } - new connection, aliases + aliases[initial_table] = 1 + new connection, aliases, type_caster end end @@ -51,45 +54,37 @@ module ActiveRecord end # table_joins is an array of arel joins which might conflict with the aliases we assign here - def initialize(connection, aliases) + def initialize(connection, aliases, type_caster) @aliases = aliases @connection = connection + @type_caster = type_caster end def aliased_table_for(table_name, aliased_name) - table_alias = aliased_name_for(table_name, aliased_name) - - if table_alias == table_name - Arel::Table.new(table_name) - else - Arel::Table.new(table_name).alias(table_alias) - end - end - - def aliased_name_for(table_name, aliased_name) if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 - table_name + Arel::Table.new(table_name, type_caster: @type_caster) else # Otherwise, we need to use an alias - aliased_name = connection.table_alias_for(aliased_name) + aliased_name = @connection.table_alias_for(aliased_name) # Update the count aliases[aliased_name] += 1 - if aliases[aliased_name] > 1 + table_alias = if aliases[aliased_name] > 1 "#{truncate(aliased_name)}_#{aliases[aliased_name]}" else aliased_name end + Arel::Table.new(table_name, type_caster: @type_caster).alias(table_alias) end end private def truncate(name) - name.slice(0, connection.table_alias_length - 2) + name.slice(0, @connection.table_alias_length - 2) end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index f1c36cd047..0d8e4ba870 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -121,7 +121,7 @@ module ActiveRecord # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all) + AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) end # Loads the \target if needed and returns it. diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 519d4d8651..2416167834 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -2,42 +2,30 @@ module ActiveRecord module Associations class AssociationScope #:nodoc: def self.scope(association, connection) - INSTANCE.scope association, connection - end - - class BindSubstitution - def initialize(block) - @block = block - end - - def bind_value(scope, column, value, alias_tracker) - substitute = alias_tracker.connection.substitute_at( - column, scope.bind_values.length) - scope.bind_values += [[column, @block.call(value)]] - substitute - end + INSTANCE.scope(association, connection) end def self.create(&block) - block = block ? block : lambda { |val| val } - new BindSubstitution.new(block) + block ||= lambda { |val| val } + new(block) end - def initialize(bind_substitution) - @bind_substitution = bind_substitution + def initialize(value_transformation) + @value_transformation = value_transformation end INSTANCE = create def scope(association, connection) - klass = association.klass - reflection = association.reflection - scope = klass.unscoped - owner = association.owner - alias_tracker = AliasTracker.empty connection + klass = association.klass + reflection = association.reflection + scope = klass.unscoped + owner = association.owner + alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster + chain_head, chain_tail = get_chain(reflection, association, alias_tracker) scope.extending! Array(reflection.options[:extend]) - add_constraints(scope, owner, klass, reflection, alias_tracker) + add_constraints(scope, owner, klass, reflection, chain_head, chain_tail) end def join_type @@ -45,137 +33,132 @@ module ActiveRecord end def self.get_bind_values(owner, chain) - bvs = [] - chain.each_with_index do |reflection, i| - if reflection == chain.last - bvs << reflection.join_id_for(owner) - if reflection.type - bvs << owner.class.base_class.name - end - else - if reflection.type - bvs << chain[i + 1].klass.base_class.name - end - end - end - bvs - end + binds = [] + last_reflection = chain.last - private + binds << last_reflection.join_id_for(owner) + if last_reflection.type + binds << owner.class.base_class.name + end - def construct_tables(chain, klass, refl, alias_tracker) - chain.map do |reflection| - alias_tracker.aliased_table_for( - table_name_for(reflection, klass, refl), - table_alias_for(reflection, refl, reflection != refl) - ) + chain.each_cons(2).each do |reflection, next_reflection| + if reflection.type + binds << next_reflection.klass.base_class.name + end end + binds end - def table_alias_for(reflection, refl, join = false) - name = "#{reflection.plural_name}_#{alias_suffix(refl)}" - name << "_join" if join - name - end + protected + + attr_reader :value_transformation + private def join(table, constraint) table.create_join(table, table.create_on(constraint), join_type) end - def column_for(table_name, column_name, alias_tracker) - columns = alias_tracker.connection.schema_cache.columns_hash(table_name) - columns[column_name] - end + def last_chain_scope(scope, table, reflection, owner, association_klass) + join_keys = reflection.join_keys(association_klass) + key = join_keys.key + foreign_key = join_keys.foreign_key + + value = transform_value(owner[foreign_key]) + scope = scope.where(table.name => { key => value }) + + if reflection.type + polymorphic_type = transform_value(owner.class.base_class.name) + scope = scope.where(table.name => { reflection.type => polymorphic_type }) + end - def bind_value(scope, column, value, alias_tracker) - @bind_substitution.bind_value scope, column, value, alias_tracker + scope end - def bind(scope, table_name, column_name, value, tracker) - column = column_for table_name, column_name, tracker - bind_value scope, column, value, tracker + def transform_value(value) + value_transformation.call(value) end - def add_constraints(scope, owner, assoc_klass, refl, tracker) - chain = refl.chain - scope_chain = refl.scope_chain + def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) + join_keys = reflection.join_keys(association_klass) + key = join_keys.key + foreign_key = join_keys.foreign_key - tables = construct_tables(chain, assoc_klass, refl, tracker) + constraint = table[key].eq(foreign_table[foreign_key]) - chain.each_with_index do |reflection, i| - table, foreign_table = tables.shift, tables.first + if reflection.type + value = transform_value(next_reflection.klass.base_class.name) + scope = scope.where(table.name => { reflection.type => value }) + end - join_keys = reflection.join_keys(assoc_klass) - key = join_keys.key - foreign_key = join_keys.foreign_key + scope = scope.joins(join(foreign_table, constraint)) + end - if reflection == chain.last - bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker - scope = scope.where(table[key].eq(bind_val)) + class ReflectionProxy < SimpleDelegator # :nodoc: + attr_accessor :next + attr_reader :alias_name - if reflection.type - value = owner.class.base_class.name - bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker - scope = scope.where(table[reflection.type].eq(bind_val)) - end - else - constraint = table[key].eq(foreign_table[foreign_key]) + def initialize(reflection, alias_name) + super(reflection) + @alias_name = alias_name + end - if reflection.type - value = chain[i + 1].klass.base_class.name - bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker - scope = scope.where(table[reflection.type].eq(bind_val)) - end + def all_includes; nil; end + end - scope = scope.joins(join(foreign_table, constraint)) - end + def get_chain(reflection, association, tracker) + name = reflection.name + runtime_reflection = Reflection::RuntimeReflection.new(reflection, association) + previous_reflection = runtime_reflection + reflection.chain.drop(1).each do |refl| + alias_name = tracker.aliased_table_for(refl.table_name, refl.alias_candidate(name)) + proxy = ReflectionProxy.new(refl, alias_name) + previous_reflection.next = proxy + previous_reflection = proxy + end + [runtime_reflection, previous_reflection] + end - is_first_chain = i == 0 - klass = is_first_chain ? assoc_klass : reflection.klass + def add_constraints(scope, owner, association_klass, refl, chain_head, chain_tail) + owner_reflection = chain_tail + table = owner_reflection.alias_name + scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass) + + reflection = chain_head + loop do + break unless reflection + table = reflection.alias_name + + unless reflection == chain_tail + next_reflection = reflection.next + foreign_table = next_reflection.alias_name + scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) + end # Exclude the scope of the association itself, because that # was already merged in the #scope method. - scope_chain[i].each do |scope_chain_item| - item = eval_scope(klass, scope_chain_item, owner) + reflection.constraints.each do |scope_chain_item| + item = eval_scope(reflection.klass, scope_chain_item, owner) if scope_chain_item == refl.scope - scope.merge! item.except(:where, :includes, :bind) + scope.merge! item.except(:where, :includes) end - if is_first_chain + reflection.all_includes do scope.includes! item.includes_values end - scope.where_values += item.where_values - scope.bind_values += item.bind_values + scope.where_clause += item.where_clause scope.order_values |= item.order_values end + + reflection = reflection.next end scope end - def alias_suffix(refl) - refl.name - end - - def table_name_for(reflection, klass, refl) - if reflection == refl - # If this is a polymorphic belongs_to, we want to get the klass from the - # association because it depends on the polymorphic_type attribute of - # the owner - klass.table_name - else - reflection.table_name - end - end - def eval_scope(klass, scope, owner) - if scope.is_a?(Relation) - scope - else - klass.unscoped.instance_exec(owner, &scope) - end + klass.unscoped.instance_exec(owner, &scope) end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 81fdd681de..265a65c4c1 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -68,16 +68,19 @@ module ActiveRecord def increment_counter(counter_cache_name) if foreign_key_present? klass.increment_counter(counter_cache_name, target_id) + if target && !stale_target? + target.increment(counter_cache_name) + end end end # Checks whether record is different to the current target, without loading it def different_target?(record) - record.id != owner[reflection.foreign_key] + record.id != owner._read_attribute(reflection.foreign_key) end def replace_keys(record) - owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)] + owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class)) end def remove_keys @@ -85,7 +88,7 @@ module ActiveRecord end def foreign_key_present? - owner[reflection.foreign_key] + owner._read_attribute(reflection.foreign_key) end # NOTE - for now, we're only supporting inverse setting from belongs_to back onto @@ -99,12 +102,13 @@ module ActiveRecord if options[:primary_key] owner.send(reflection.name).try(:id) else - owner[reflection.foreign_key] + owner._read_attribute(reflection.foreign_key) end end def stale_state - owner[reflection.foreign_key] && owner[reflection.foreign_key].to_s + result = owner._read_attribute(reflection.foreign_key) + result && result.to_s end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index f085fd1cfd..88406740d8 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/module/attribute_accessors' - # This is the parent Association class which defines the variables # used by all associations. # @@ -15,15 +13,10 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: class << self attr_accessor :extensions - # TODO: This class accessor is needed to make activerecord-deprecated_finders work. - # We can move it to a constant in 5.0. - attr_accessor :valid_options end self.extensions = [] - self.valid_options = [:class_name, :class, :foreign_key, :validate] - - attr_reader :name, :scope, :options + VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] # :nodoc: def self.build(model, name, scope, options, &block) if model.dangerous_attribute_method?(name) @@ -32,60 +25,68 @@ module ActiveRecord::Associations::Builder "Please choose a different association name." end - builder = create_builder model, name, scope, options, &block - reflection = builder.build(model) + extension = define_extensions model, name, &block + reflection = create_reflection model, name, scope, options, extension define_accessors model, reflection define_callbacks model, reflection - builder.define_extensions model + define_validations model, reflection reflection end - def self.create_builder(model, name, scope, options, &block) + def self.create_reflection(model, name, scope, options, extension = nil) raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) - new(model, name, scope, options, &block) - end - - def initialize(model, name, scope, options) - # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders. if scope.is_a?(Hash) options = scope scope = nil end - # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders. - @name = name - @scope = scope - @options = options + validate_options(options) - validate_options + scope = build_scope(scope, extension) + + ActiveRecord::Reflection.create(macro, name, scope, options, model) + end + + def self.build_scope(scope, extension) + new_scope = scope if scope && scope.arity == 0 - @scope = proc { instance_exec(&scope) } + new_scope = proc { instance_exec(&scope) } + end + + if extension + new_scope = wrap_scope new_scope, extension end + + new_scope end - def build(model) - ActiveRecord::Reflection.create(macro, name, scope, options, model) + def self.wrap_scope(scope, extension) + scope end - def macro + def self.macro raise NotImplementedError end - def valid_options - Association.valid_options + Association.extensions.flat_map(&:valid_options) + def self.valid_options(options) + VALID_OPTIONS + Association.extensions.flat_map(&:valid_options) end - def validate_options - options.assert_valid_keys(valid_options) + def self.validate_options(options) + options.assert_valid_keys(valid_options(options)) end - def define_extensions(model) + def self.define_extensions(model, name) end def self.define_callbacks(model, reflection) - add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent] + if dependent = reflection.options[:dependent] + check_dependent_options(dependent) + add_destroy_callbacks(model, reflection) + end + Association.extensions.each do |extension| extension.build model, reflection end @@ -120,17 +121,21 @@ module ActiveRecord::Associations::Builder CODE end + def self.define_validations(model, reflection) + # noop + end + def self.valid_dependent_options raise NotImplementedError end - private - - def self.add_before_destroy_callbacks(model, reflection) - unless valid_dependent_options.include? reflection.options[:dependent] - raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}" + def self.check_dependent_options(dependent) + unless valid_dependent_options.include? dependent + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}" end + end + def self.add_destroy_callbacks(model, reflection) name = reflection.name model.before_destroy lambda { |o| o.association(name).handle_dependency } end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 3998aca23e..d0ad57f9c6 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: - def macro + def self.macro :belongs_to end - def valid_options + def self.valid_options(options) super + [:foreign_type, :polymorphic, :touch, :counter_cache] end @@ -23,8 +23,6 @@ module ActiveRecord::Associations::Builder add_counter_cache_methods mixin end - private - def self.add_counter_cache_methods(mixin) return if mixin.method_defined? :belongs_to_counter_cache_after_update @@ -107,5 +105,10 @@ module ActiveRecord::Associations::Builder model.after_touch callback model.after_destroy callback end + + def self.add_destroy_callbacks(model, reflection) + name = reflection.name + model.after_destroy lambda { |o| o.association(name).handle_dependency } + end end end diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index bc15a49996..2ff67f904d 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -7,22 +7,11 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] - def valid_options + def self.valid_options(options) super + [:table_name, :before_add, :after_add, :before_remove, :after_remove, :extend] end - attr_reader :block_extension - - def initialize(model, name, scope, options) - super - @mod = nil - if block_given? - @mod = Module.new(&Proc.new) - @scope = wrap_scope @scope, @mod - end - end - def self.define_callbacks(model, reflection) super name = reflection.name @@ -32,10 +21,11 @@ module ActiveRecord::Associations::Builder } end - def define_extensions(model) - if @mod + def self.define_extensions(model, name) + if block_given? extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" - model.parent.const_set(extension_module_name, @mod) + extension = Module.new(&Proc.new) + model.parent.const_set(extension_module_name, extension) end end @@ -78,9 +68,7 @@ module ActiveRecord::Associations::Builder CODE end - private - - def wrap_scope(scope, mod) + def self.wrap_scope(scope, mod) if scope proc { |owner| instance_exec(owner, &scope).extending(mod) } else diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 0ad5206980..93dc4ae118 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -11,11 +11,14 @@ module ActiveRecord::Associations::Builder end def join_table - @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") end private - def klass; @rhs_class_name.constantize; end + + def klass + @lhs_class.send(:compute_type, @rhs_class_name) + end end def self.build(lhs_class, name, options) @@ -23,13 +26,7 @@ module ActiveRecord::Associations::Builder KnownTable.new options[:join_table].to_s else class_name = options.fetch(:class_name) { - model_name = name.to_s.camelize.singularize - - if lhs_class.parent_name - model_name.prepend("#{lhs_class.parent_name}::") - end - - model_name + name.to_s.camelize.singularize } KnownClass.new lhs_class, class_name end @@ -90,18 +87,18 @@ module ActiveRecord::Associations::Builder middle_name = [lhs_model.name.downcase.pluralize, association_name].join('_').gsub(/::/, '_').to_sym middle_options = middle_options join_model - hm_builder = HasMany.create_builder(lhs_model, - middle_name, - nil, - middle_options) - hm_builder.build lhs_model + + HasMany.create_reflection(lhs_model, + middle_name, + nil, + middle_options) end private def middle_options(join_model) middle_options = {} - middle_options[:class] = join_model + middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}" middle_options[:source] = join_model.left_reflection.name if options.key? :foreign_key middle_options[:foreign_key] = options[:foreign_key] @@ -113,7 +110,7 @@ module ActiveRecord::Associations::Builder rhs_options = {} if options.key? :class_name - rhs_options[:foreign_key] = options[:class_name].foreign_key + rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key rhs_options[:class_name] = options[:class_name] end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 4c8c826f76..1c1b47bd56 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,11 +1,11 @@ module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: - def macro + def self.macro :has_many end - def valid_options - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table] + def self.valid_options(options) + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type] end def self.valid_dependent_options diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index a1f4f51664..64e9e6b334 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,11 +1,11 @@ module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: - def macro + def self.macro :has_one end - def valid_options - valid = super + [:as] + def self.valid_options(options) + valid = super + [:as, :foreign_type] valid += [:through, :source, :source_type] if options[:through] valid end @@ -14,9 +14,7 @@ module ActiveRecord::Associations::Builder [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception] end - private - - def self.add_before_destroy_callbacks(model, reflection) + def self.add_destroy_callbacks(model, reflection) super unless reflection.options[:through] end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index e655c389a6..f6274c027e 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -2,8 +2,8 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: - def valid_options - super + [:remote, :dependent, :primary_key, :inverse_of] + def self.valid_options(options) + super + [:dependent, :primary_key, :inverse_of, :required] end def self.define_accessors(model, reflection) @@ -27,5 +27,12 @@ module ActiveRecord::Associations::Builder end CODE end + + def self.define_validations(model, reflection) + super + if reflection.options[:required] + model.validates_presence_of reflection.name, message: :required + end + end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 306588ac66..eea847cfa3 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -33,7 +33,13 @@ module ActiveRecord reload end - @proxy ||= CollectionProxy.create(klass, self) + if owner.new_record? + # Cache the proxy separately before the owner has an id + # or else a post-save proxy will still lack the id + @new_record_proxy ||= CollectionProxy.create(klass, self) + else + @proxy ||= CollectionProxy.create(klass, self) + end end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -55,10 +61,10 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_column = reflection.primary_key_column - ids = Array(ids).reject { |id| id.blank? } - ids.map! { |i| pk_column.type_cast_from_user(i) } - replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) + pk_type = reflection.primary_key_type + ids = Array(ids).reject(&:blank?) + ids.map! { |i| pk_type.cast(i) } + replace(klass.find(ids).index_by(&:id).values_at(*ids)) end def reset @@ -145,6 +151,7 @@ module ActiveRecord # be chained. Since << flattens its argument list and inserts each record, # +push+ and +concat+ behave identically. def concat(*records) + records = records.flatten if owner.new_record? load_target concat_records(records) @@ -169,8 +176,8 @@ module ActiveRecord end # Removes all records from the association without calling callbacks - # on the associated records. It honors the `:dependent` option. However - # if the `:dependent` value is `:destroy` then in that case the `:delete_all` + # on the associated records. It honors the +:dependent+ option. However + # if the +:dependent+ value is +:destroy+ then in that case the +:delete_all+ # deletion strategy for the association is applied. # # You can force a particular deletion strategy by passing a parameter. @@ -212,11 +219,7 @@ module ActiveRecord # Count all records using SQL. Construct options and pass them with # scope to the target class's +count+. - def count(column_name = nil, count_options = {}) - # TODO: Remove count_options argument as soon we remove support to - # activerecord-deprecated_finders. - column_name, count_options = nil, column_name if column_name.is_a?(Hash) - + def count(column_name = nil) relation = scope if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. @@ -283,7 +286,7 @@ module ActiveRecord elsif !loaded? && !association_scope.group_values.empty? load_target.size elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array) - unsaved_records = target.select { |r| r.new_record? } + unsaved_records = target.select(&:new_record?) unsaved_records.size + count_records else count_records @@ -316,7 +319,8 @@ module ActiveRecord end # Returns true if the collections is not empty. - # Equivalent to +!collection.empty?+. + # If block given, loads all records and checks for one or more matches. + # Otherwise, equivalent to +!collection.empty?+. def any? if block_given? load_target.any? { |*block_args| yield(*block_args) } @@ -326,7 +330,8 @@ module ActiveRecord end # Returns true if the collection has more than 1 record. - # Equivalent to +collection.size > 1+. + # If block given, loads all records and checks for two or more matches. + # Otherwise, equivalent to +collection.size > 1+. def many? if block_given? load_target.many? { |*block_args| yield(*block_args) } @@ -352,6 +357,7 @@ module ActiveRecord if owner.new_record? replace_records(other_array, original_target) else + replace_common_records_in_memory(other_array, original_target) if other_array != original_target transaction { replace_records(other_array, original_target) } end @@ -379,11 +385,18 @@ module ActiveRecord target end - def add_to_target(record, skip_callbacks = false) + def add_to_target(record, skip_callbacks = false, &block) + if association_scope.distinct_value + index = @target.index(record) + end + replace_on_target(record, index, skip_callbacks, &block) + end + + def replace_on_target(record, index, skip_callbacks) callback(:before_add, record) unless skip_callbacks yield(record) if block_given? - if association_scope.distinct_value && index = @target.index(record) + if index @target[index] = record else @target << record @@ -407,7 +420,13 @@ module ActiveRecord private def get_records - return scope.to_a if reflection.scope_chain.any?(&:any?) + if reflection.scope_chain.any?(&:any?) || + scope.eager_loading? || + klass.current_scope || + klass.default_scopes.any? + + return scope.to_a + end conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do @@ -486,7 +505,7 @@ module ActiveRecord def delete_or_destroy(records, method) records = records.flatten records.each { |record| raise_on_type_mismatch!(record) } - existing_records = records.reject { |r| r.new_record? } + existing_records = records.reject(&:new_record?) if existing_records.empty? remove_records(existing_records, records, method) @@ -522,10 +541,18 @@ module ActiveRecord target end + def replace_common_records_in_memory(new_target, original_target) + common_records = new_target & original_target + common_records.each do |record| + skip_callbacks = true + replace_on_target(record, @target.index(record), skip_callbacks) + end + end + def concat_records(records, should_raise = false) result = true - records.flatten.each do |record| + records.each do |record| raise_on_type_mismatch!(record) add_to_target(record) do |rec| result &&= insert_record(rec, true, should_raise) unless owner.new_record? @@ -569,8 +596,8 @@ module ActiveRecord if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) assoc = owner.association(reflection.through_reflection.name) assoc.reader.any? { |source| - target = source.send(reflection.source_reflection.name) - target.respond_to?(:include?) ? target.include?(record) : target == record + target_reflection = source.send(reflection.source_reflection.name) + target_reflection.respond_to?(:include?) ? target_reflection.include?(record) : target_reflection == record } || target.include?(record) else target.include?(record) @@ -581,7 +608,7 @@ module ActiveRecord # specified, then #find scans the entire collection. def find_by_scan(*args) expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq + ids = args.flatten.compact.map(&:to_s).uniq if ids.size == 1 id = ids.first diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 84c8cfe72b..c22dc6e11e 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -29,10 +29,11 @@ module ActiveRecord # instantiation of the actual post records. class CollectionProxy < Relation delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope) + delegate :find_nth, to: :scope def initialize(klass, association) #:nodoc: @association = association - super klass, klass.arel_table + super klass, klass.arel_table, klass.predicate_builder merge! association.scope(nullify: false) end @@ -355,14 +356,15 @@ module ActiveRecord @association.replace(other_array) end - # Deletes all the records from the collection. For +has_many+ associations, - # the deletion is done according to the strategy specified by the <tt>:dependent</tt> - # option. + # Deletes all the records from the collection according to the strategy + # specified by the +:dependent+ option. If no +:dependent+ option is given, + # then it will follow the default strategy. # - # If no <tt>:dependent</tt> option is given, then it will follow the - # default strategy. The default strategy is <tt>:nullify</tt>. This - # sets the foreign keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, - # the default strategy is +delete_all+. + # For +has_many :through+ associations, the default deletion strategy is + # +:delete_all+. + # + # For +has_many+ associations, the default deletion strategy is +:nullify+. + # This sets the foreign keys to +NULL+. # # class Person < ActiveRecord::Base # has_many :pets # dependent: :nullify option by default @@ -393,9 +395,9 @@ module ActiveRecord # # #<Pet id: 3, name: "Choo-Choo", person_id: nil> # # ] # - # If it is set to <tt>:destroy</tt> all the objects from the collection - # are removed by calling their +destroy+ method. See +destroy+ for more - # information. + # Both +has_many+ and +has_many :through+ dependencies default to the + # +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+. + # Records are not instantiated and callbacks will not be fired. # # class Person < ActiveRecord::Base # has_many :pets, dependent: :destroy @@ -410,11 +412,6 @@ module ActiveRecord # # ] # # person.pets.delete_all - # # => [ - # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, - # # #<Pet id: 2, name: "Spook", person_id: 1>, - # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> - # # ] # # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound @@ -443,8 +440,9 @@ module ActiveRecord end # Deletes the records of the collection directly from the database - # ignoring the +:dependent+ option. It invokes +before_remove+, - # +after_remove+ , +before_destroy+ and +after_destroy+ callbacks. + # ignoring the +:dependent+ option. Records are instantiated and it + # invokes +before_remove+, +after_remove+ , +before_destroy+ and + # +after_destroy+ callbacks. # # class Person < ActiveRecord::Base # has_many :pets @@ -690,10 +688,8 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def count(column_name = nil, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - @association.count(column_name, options) + def count(column_name = nil) + @association.count(column_name) end # Returns the size of the collection. If the collection hasn't been loaded, @@ -783,7 +779,7 @@ module ActiveRecord # person.pets.count # => 0 # person.pets.any? # => true # - # You can also pass a block to define criteria. The behavior + # You can also pass a +block+ to define criteria. The behavior # is the same, it returns true if the collection based on the # criteria is not empty. # @@ -817,7 +813,7 @@ module ActiveRecord # person.pets.count # => 2 # person.pets.many? # => true # - # You can also pass a block to define criteria. The + # You can also pass a +block+ to define criteria. The # behavior is the same, it returns true if the collection # based on the criteria has more than one record. # @@ -841,7 +837,7 @@ module ActiveRecord @association.many?(&block) end - # Returns +true+ if the given object is present in the collection. + # Returns +true+ if the given +record+ is present in the collection. # # class Person < ActiveRecord::Base # has_many :pets @@ -879,7 +875,7 @@ module ActiveRecord # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays # contain the same number of elements and if each element is equal - # to the corresponding element in the other array, otherwise returns + # to the corresponding element in the +other+ array, otherwise returns # +false+. # # class Person < ActiveRecord::Base diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb new file mode 100644 index 0000000000..fe48ecec29 --- /dev/null +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -0,0 +1,11 @@ +module ActiveRecord::Associations + module ForeignAssociation + def foreign_key_present? + if reflection.klass.primary_key + owner.attribute_present?(reflection.active_record_primary_key) + else + false + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 2727e23870..ca27c9fdde 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -6,6 +6,7 @@ module ActiveRecord # If the association has a <tt>:through</tt> option further specialization # is provided by its child HasManyThroughAssociation. class HasManyAssociation < CollectionAssociation #:nodoc: + include ForeignAssociation def handle_dependency case options[:dependent] @@ -16,7 +17,7 @@ module ActiveRecord unless empty? record = klass.human_attribute_name(reflection.name).downcase owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) - false + throw(:abort) end else @@ -41,6 +42,14 @@ module ActiveRecord end end + def empty? + if has_cached_counter? + size.zero? + else + super + end + end + private # Returns the number of records in this collection. @@ -58,7 +67,7 @@ module ActiveRecord # the loaded flag is set to true as well. def count_records count = if has_cached_counter? - owner.read_attribute cached_counter_attribute_name + owner._read_attribute cached_counter_attribute_name else scope.count end @@ -76,15 +85,30 @@ module ActiveRecord end def cached_counter_attribute_name(reflection = reflection()) - options[:counter_cache] || "#{reflection.name}_count" + if reflection.options[:counter_cache] + reflection.options[:counter_cache].to_s + else + "#{reflection.name}_count" + end end def update_counter(difference, reflection = reflection()) + update_counter_in_database(difference, reflection) + update_counter_in_memory(difference, reflection) + end + + def update_counter_in_database(difference, reflection = reflection()) if has_cached_counter?(reflection) counter = cached_counter_attribute_name(reflection) owner.class.update_counters(owner.id, counter => difference) + end + end + + def update_counter_in_memory(difference, reflection = reflection()) + if counter_must_be_updated_by_has_many?(reflection) + counter = cached_counter_attribute_name(reflection) owner[counter] += difference - owner.changed_attributes.delete(counter) # eww + owner.send(:clear_attribute_changes, counter) # eww end end @@ -98,14 +122,28 @@ module ActiveRecord # it will be decremented twice. # # Hence this method. - def inverse_updates_counter_cache?(reflection = reflection()) + def inverse_which_updates_counter_cache(reflection = reflection()) counter_name = cached_counter_attribute_name(reflection) - reflection.klass._reflections.values.any? { |inverse_reflection| - :belongs_to == inverse_reflection.macro && + inverse_which_updates_counter_named(counter_name, reflection) + end + alias inverse_updates_counter_cache? inverse_which_updates_counter_cache + + def inverse_which_updates_counter_named(counter_name, reflection) + reflection.klass._reflections.values.find { |inverse_reflection| + inverse_reflection.belongs_to? && inverse_reflection.counter_cache_column == counter_name } end + def inverse_updates_counter_in_memory?(reflection) + inverse = inverse_which_updates_counter_cache(reflection) + inverse && inverse == reflection.inverse_of + end + + def counter_must_be_updated_by_has_many?(reflection) + !inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection) + end + def delete_count(method, scope) if method == :delete_all scope.delete_all @@ -130,12 +168,23 @@ module ActiveRecord end end - def foreign_key_present? - if reflection.klass.primary_key - owner.attribute_present?(reflection.association_primary_key) + def concat_records(records, *) + update_counter_if_success(super, records.length) + end + + def _create_record(attributes, *) + if attributes.is_a?(Array) + super else - false + update_counter_if_success(super, 1) + end + end + + def update_counter_if_success(saved_successfully, difference) + if saved_successfully + update_counter_in_memory(difference) end + saved_successfully end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 1713ab7ed3..4897ec44e9 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Has Many Through Association module Associations @@ -12,21 +11,6 @@ module ActiveRecord @through_association = nil end - # Returns the size of the collection by executing a SELECT COUNT(*) query - # if the collection hasn't been loaded, and by calling collection.size if - # it has. If the collection will likely have a size greater than zero, - # and if fetching the collection will be needed afterwards, one less - # SELECT query will be generated by using #length instead. - def size - if has_cached_counter? - owner.read_attribute cached_counter_attribute_name(reflection) - elsif loaded? - target.size - else - super - end - end - def concat(*records) unless owner.new_record? records.flatten.each do |record| @@ -63,7 +47,7 @@ module ActiveRecord end save_through_record(record) - update_counter(1) + record end @@ -82,14 +66,20 @@ module ActiveRecord @through_records[record.object_id] ||= begin ensure_mutable - through_record = through_association.build through_scope_attributes + through_record = through_association.build(*options_for_through_record) through_record.send("#{source_reflection.name}=", record) through_record end end + def options_for_through_record + [through_scope_attributes] + end + def through_scope_attributes - scope.where_values_hash(through_association.reflection.name.to_s).except!(through_association.reflection.foreign_key) + scope.where_values_hash(through_association.reflection.name.to_s). + except!(through_association.reflection.foreign_key, + through_association.reflection.klass.inheritance_column) end def save_through_record(record) @@ -145,17 +135,15 @@ module ActiveRecord if scope.klass.primary_key count = scope.destroy_all.length else - scope.to_a.each do |record| - record.run_callbacks :destroy - end + scope.each(&:_run_destroy_callbacks) arel = scope.arel - stmt = Arel::DeleteManager.new arel.engine + stmt = Arel::DeleteManager.new stmt.from scope.klass.arel_table stmt.wheres = arel.constraints - count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values) + count = scope.klass.connection.delete(stmt, 'SQL', scope.bound_attributes) end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) @@ -172,9 +160,9 @@ module ActiveRecord if through_reflection.collection? && update_through_counter?(method) update_counter(-count, through_reflection) + else + update_counter(-count) end - - update_counter(-count) end def through_records_for(record) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 944caacab6..41a75b820e 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,8 +1,8 @@ - module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: + include ForeignAssociation def handle_dependency case options[:dependent] @@ -13,7 +13,7 @@ module ActiveRecord if load_target record = klass.human_attribute_name(reflection.name).downcase owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record) - false + throw(:abort) end else diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index fbb4551b22..fcf06323e6 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -20,7 +20,7 @@ module ActiveRecord end def columns - @tables.flat_map { |t| t.column_aliases } + @tables.flat_map(&:column_aliases) end # An array of [column_name, alias] pairs for the table @@ -93,8 +93,7 @@ module ActiveRecord # joins # => [] # def initialize(base, associations, joins) - @alias_tracker = AliasTracker.create(base.connection, joins) - @alias_tracker.aliased_name_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1 + @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins, base.type_caster) tree = self.class.make_tree associations @join_root = JoinBase.new base, build(tree, base) @join_root.children.each { |child| construct_tables! @join_root, child } @@ -131,7 +130,6 @@ module ActiveRecord def instantiate(result_set, aliases) primary_key = aliases.column_alias(join_root, join_root.primary_key) - type_caster = result_set.column_type primary_key seen = Hash.new { |h,parent_klass| h[parent_klass] = Hash.new { |i,parent_id| @@ -143,12 +141,20 @@ module ActiveRecord parents = model_cache[join_root] column_aliases = aliases.column_aliases join_root - result_set.each { |row_hash| - primary_id = type_caster.type_cast_from_database row_hash[primary_key] - parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases) - construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + message_bus = ActiveSupport::Notifications.instrumenter + + payload = { + record_count: result_set.length, + class_name: join_root.base_klass.name } + message_bus.instrument('instantiation.active_record', payload) do + result_set.each { |row_hash| + parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) + construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + } + end + parents.values end @@ -226,6 +232,7 @@ module ActiveRecord end def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + return if ar_parent.nil? primary_id = ar_parent.id parent.children.each do |node| @@ -242,7 +249,11 @@ module ActiveRecord key = aliases.column_alias(node, node.primary_key) id = row[key] - next if id.nil? + if id.nil? + nil_association = ar_parent.association(node.reflection.name) + nil_association.loaded! + next + end model = seen[parent.base_klass][primary_id][node.base_klass][id] @@ -250,6 +261,7 @@ module ActiveRecord construct(model, node, row, rs, seen, model_cache, aliases) else model = construct_model(ar_parent, node, row, model_cache, id, aliases) + model.readonly! seen[parent.base_klass][primary_id][node.base_klass][id] = model construct(model, node, row, rs, seen, model_cache, aliases) end 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 a0e83c0a02..a6ad09a38a 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -25,7 +25,7 @@ module ActiveRecord def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) joins = [] - bind_values = [] + binds = [] tables = tables.reverse scope_chain_index = 0 @@ -37,43 +37,45 @@ module ActiveRecord table = tables.shift klass = reflection.klass - case reflection.source_macro - when :belongs_to - key = reflection.association_primary_key - foreign_key = reflection.foreign_key - else - key = reflection.foreign_key - foreign_key = reflection.active_record_primary_key - end + join_keys = reflection.join_keys(klass) + key = join_keys.key + foreign_key = join_keys.foreign_key constraint = build_constraint(klass, table, key, foreign_table, foreign_key) + predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) scope_chain_items = scope_chain[scope_chain_index].map do |item| if item.is_a?(Relation) item else - ActiveRecord::Relation.create(klass, table).instance_exec(node, &item) + ActiveRecord::Relation.create(klass, table, predicate_builder) + .instance_exec(node, &item) end end scope_chain_index += 1 - scope_chain_items.concat [klass.send(:build_default_scope, ActiveRecord::Relation.create(klass, table))].compact + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right end if rel && !rel.arel.constraints.empty? - bind_values.concat rel.bind_values + binds += rel.bound_attributes constraint = constraint.and rel.arel.constraints end if reflection.type value = foreign_klass.base_class.name - column = klass.columns_hash[column.to_s] + column = klass.columns_hash[reflection.type.to_s] - substitute = klass.connection.substitute_at(column, bind_values.length) - bind_values.push [column, value] + substitute = klass.connection.substitute_at(column) + binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name)) constraint = constraint.and table[reflection.type].eq substitute end @@ -83,7 +85,7 @@ module ActiveRecord foreign_table, foreign_klass = table, klass end - JoinInformation.new joins, bind_values + JoinInformation.new joins, binds end # Builds equality condition. @@ -95,7 +97,7 @@ module ActiveRecord # end # # If I execute `Physician.joins(:appointments).to_a` then - # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...> + # klass # => Physician # table # => #<Arel::Table @name="appointments" ...> # key # => physician_id # foreign_table # => #<Arel::Table @name="physicians" ...> 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 91e1c6a9d7..9c6573f913 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -19,7 +19,6 @@ module ActiveRecord def initialize(base_klass, children) @base_klass = base_klass - @column_names_with_alias = nil @children = children end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 7519fec10a..97f4bd3811 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -2,33 +2,42 @@ module ActiveRecord module Associations # Implements the details of eager loading of Active Record associations. # - # Note that 'eager loading' and 'preloading' are actually the same thing. - # However, there are two different eager loading strategies. + # Suppose that you have the following two Active Record models: # - # The first one is by using table joins. This was only strategy available - # prior to Rails 2.1. Suppose that you have an Author model with columns - # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using - # this strategy, Active Record would try to retrieve all data for an author - # and all of its books via a single query: + # class Author < ActiveRecord::Base + # # columns: name, age + # has_many :books + # end # - # SELECT * FROM authors - # LEFT OUTER JOIN books ON authors.id = books.author_id - # WHERE authors.name = 'Ken Akamatsu' + # class Book < ActiveRecord::Base + # # columns: title, sales + # end # - # However, this could result in many rows that contain redundant data. After - # having received the first row, we already have enough data to instantiate - # the Author object. In all subsequent rows, only the data for the joined - # 'books' table is useful; the joined 'authors' data is just redundant, and - # processing this redundant data takes memory and CPU time. The problem - # quickly becomes worse and worse as the level of eager loading increases - # (i.e. if Active Record is to eager load the associations' associations as - # well). + # When you load an author with all associated books Active Record will make + # multiple queries like this: + # + # Author.includes(:books).where(:name => ['bell hooks', 'Homer').to_a + # + # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer') + # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5) + # + # Active Record saves the ids of the records from the first query to use in + # the second. Depending on the number of associations involved there can be + # arbitrarily many SQL queries made. + # + # However, if there is a WHERE clause that spans across tables Active + # Record will fall back to a slightly more resource-intensive single query: + # + # Author.includes(:books).where(books: {title: 'Illiad'}).to_a + # => SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2, + # `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`sales` AS t1_r2 + # FROM `authors` + # LEFT OUTER JOIN `books` ON `authors`.`id` = `books`.`author_id` + # WHERE `books`.`title` = 'Illiad' + # + # This could result in many rows that contain redundant data and it performs poorly at scale + # and is therefore only used when necessary. # - # The second strategy is to use multiple database queries, one for each - # level of association. Since Rails 2.1, this is the default strategy. In - # situations where a table join is necessary (e.g. when the +:conditions+ - # option references an association's column), it will fallback to the table - # join strategy. class Preloader #:nodoc: extend ActiveSupport::Autoload @@ -80,7 +89,7 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] - NULL_RELATION = Struct.new(:values, :bind_values).new({}, []) + NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) def preload(records, associations, preload_scope = nil) records = Array.wrap(records).compact.uniq @@ -169,6 +178,7 @@ module ActiveRecord class NullPreloader def self.new(klass, owners, reflection, preload_scope); self; end def self.run(preloader); end + def self.preloaded_records; []; end end def preloader_for(reflection, owners, rhs_klass) diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 33c8619359..1dc8bff193 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -33,7 +33,7 @@ module ActiveRecord end def query_scope(ids) - scope.where(association_key.in(ids)) + scope.where(association_key_name => ids) end def table @@ -104,11 +104,11 @@ module ActiveRecord end def association_key_type - @klass.column_for_attribute(association_key_name).type + @klass.type_for_attribute(association_key_name.to_s).type end def owner_key_type - @model.column_for_attribute(owner_key_name).type + @model.type_for_attribute(owner_key_name.to_s).type end def load_slices(slices) @@ -131,30 +131,30 @@ module ActiveRecord def build_scope scope = klass.unscoped - values = reflection_scope.values - reflection_binds = reflection_scope.bind_values + values = reflection_scope.values preload_values = preload_scope.values - preload_binds = preload_scope.bind_values - scope.where_values = Array(values[:where]) + Array(preload_values[:where]) + scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause scope.references_values = Array(values[:references]) + Array(preload_values[:references]) - scope.bind_values = (reflection_binds + preload_binds) - scope._select! preload_values[:select] || values[:select] || table[Arel.star] + scope._select! preload_values[:select] || values[:select] || table[Arel.star] scope.includes! preload_values[:includes] || values[:includes] - - if preload_values.key? :order - scope.order! preload_values[:order] + if preload_scope.joins_values.any? + scope.joins!(preload_scope.joins_values) else - if values.key? :order - scope.order! values[:order] - end + scope.joins!(reflection_scope.joins_values) + end + scope.order! preload_values[:order] || values[:order] + + if preload_values[:readonly] || values[:readonly] + scope.readonly! end if options[:as] scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end + scope.unscope_values = Array(values[:unscope]) klass.default_scoped.merge(scope) 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 7b37b5942d..2029871f39 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -8,7 +8,7 @@ module ActiveRecord records_by_owner = super if reflection_scope.distinct_value - records_by_owner.each_value { |records| records.uniq! } + records_by_owner.each_value(&:uniq!) end records_by_owner diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 1fed7f74e7..56aa23b173 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -63,7 +63,7 @@ module ActiveRecord should_reset = (through_scope != through_reflection.klass.unscoped) || (reflection.options[:source_type] && through_reflection.collection?) - # Dont cache the association - we would only be caching a subset + # Don't cache the association - we would only be caching a subset if should_reset owners.each { |owner| owner.association(association_name).reset @@ -78,9 +78,9 @@ module ActiveRecord if options[:source_type] scope.where! reflection.foreign_type => options[:source_type] else - unless reflection_scope.where_values.empty? + unless reflection_scope.where_clause.empty? scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) - scope.where_values = reflection_scope.values[:where] + scope.where_clause = reflection_scope.where_clause end scope.references! reflection_scope.values[:references] diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index f2e3a4e40f..c44242a0f0 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -39,7 +39,13 @@ module ActiveRecord end def get_records - return scope.limit(1).to_a if reflection.scope_chain.any?(&:any?) + if reflection.scope_chain.any?(&:any?) || + scope.eager_loading? || + klass.current_scope || + klass.default_scopes.any? + + return scope.limit(1).to_a + end conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f00fef8b9e..3ce9cffdbc 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,7 +3,7 @@ module ActiveRecord module Associations module ThroughAssociation #:nodoc: - delegate :source_reflection, :through_reflection, :chain, :to => :reflection + delegate :source_reflection, :through_reflection, :to => :reflection protected @@ -13,9 +13,13 @@ module ActiveRecord # 2. To get the type conditions for any STI models in the chain def target_scope scope = super - chain.drop(1).each do |reflection| + reflection.chain.drop(1).each do |reflection| relation = reflection.klass.all - relation.merge!(reflection.scope) if reflection.scope + + reflection_scope = reflection.scope + if reflection_scope && reflection_scope.arity.zero? + relation = relation.merge(reflection_scope) + end scope.merge!( relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load) @@ -77,7 +81,7 @@ module ActiveRecord end def ensure_mutable - if source_reflection.macro != :belongs_to + unless source_reflection.belongs_to? raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) end end @@ -87,6 +91,17 @@ module ActiveRecord raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) end end + + def build_record(attributes) + inverse = source_reflection.inverse_of + target = through_association.target + + if inverse && target && !target.is_a?(Array) + attributes[inverse.foreign_key] = target.id + end + + super(attributes) + end end end end diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index f78cde23c5..73dd3fa041 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -1,38 +1,103 @@ module ActiveRecord class Attribute # :nodoc: class << self - def from_database(value, type) - FromDatabase.new(value, type) + def from_database(name, value, type) + FromDatabase.new(name, value, type) end - def from_user(value, type) - FromUser.new(value, type) + def from_user(name, value, type) + FromUser.new(name, value, type) + end + + def with_cast_value(name, value, type) + WithCastValue.new(name, value, type) + end + + def null(name) + Null.new(name) + end + + def uninitialized(name, type) + Uninitialized.new(name, type) end end - attr_reader :value_before_type_cast, :type + attr_reader :name, :value_before_type_cast, :type # This method should not be called directly. # Use #from_database or #from_user - def initialize(value_before_type_cast, type) + def initialize(name, value_before_type_cast, type) + @name = name @value_before_type_cast = value_before_type_cast @type = type end def value # `defined?` is cheaper than `||=` when we get back falsy values - @value = type_cast(value_before_type_cast) unless defined?(@value) + @value = original_value unless defined?(@value) @value end + def original_value + type_cast(value_before_type_cast) + end + def value_for_database - type.type_cast_for_database(value) + type.serialize(value) + end + + def changed_from?(old_value) + type.changed?(old_value, value, value_before_type_cast) + end + + def changed_in_place_from?(old_value) + has_been_read? && type.changed_in_place?(old_value, value) + end + + def with_value_from_user(value) + self.class.from_user(name, value, type) end - def type_cast + def with_value_from_database(value) + self.class.from_database(name, value, type) + end + + def with_cast_value(value) + self.class.with_cast_value(name, value, type) + end + + def with_type(type) + self.class.new(name, value_before_type_cast, type) + end + + def type_cast(*) raise NotImplementedError end + def initialized? + true + end + + def came_from_user? + false + end + + def has_been_read? + defined?(@value) + end + + def ==(other) + self.class == other.class && + name == other.name && + value_before_type_cast == other.value_before_type_cast && + type == other.type + end + alias eql? == + + def hash + [self.class, name, value_before_type_cast, type].hash + end + protected def initialize_dup(other) @@ -41,16 +106,69 @@ module ActiveRecord end end - class FromDatabase < Attribute + class FromDatabase < Attribute # :nodoc: def type_cast(value) - type.type_cast_from_database(value) + type.deserialize(value) + end + end + + class FromUser < Attribute # :nodoc: + def type_cast(value) + type.cast(value) + end + + def came_from_user? + true end end - class FromUser < Attribute + class WithCastValue < Attribute # :nodoc: def type_cast(value) - type.type_cast_from_user(value) + value + end + + def changed_in_place_from?(old_value) + false + end + end + + class Null < Attribute # :nodoc: + def initialize(name) + super(name, nil, Type::Value.new) + end + + def value + nil + end + + def with_type(type) + self.class.with_cast_value(name, nil, type) + end + + def with_value_from_database(value) + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" + end + alias_method :with_value_from_user, :with_value_from_database + end + + class Uninitialized < Attribute # :nodoc: + def initialize(name, type) + super(name, nil, type) + end + + def value + if block_given? + yield name + end + end + + def value_for_database + end + + def initialized? + false end end + private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 40e2918777..483d4200bd 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -3,63 +3,32 @@ require 'active_model/forbidden_attributes_protection' module ActiveRecord module AttributeAssignment extend ActiveSupport::Concern - include ActiveModel::ForbiddenAttributesProtection + include ActiveModel::AttributeAssignment - # Allows you to set all the attributes by passing in a hash of attributes with - # keys matching the attribute names (which again matches the column names). - # - # If the passed hash responds to <tt>permitted?</tt> method and the return value - # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> - # exception is raised. - # - # cat = Cat.new(name: "Gorby", status: "yawning") - # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil} - # cat.assign_attributes(status: "sleeping") - # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil } - # - # New attributes will be persisted in the database when the object is saved. - # - # Aliased to <tt>attributes=</tt>. - def assign_attributes(new_attributes) - if !new_attributes.respond_to?(:stringify_keys) - raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." - end - return if new_attributes.blank? + # Alias for `assign_attributes`. See +ActiveModel::AttributeAssignment+. + def attributes=(attributes) + assign_attributes(attributes) + end - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] - nested_parameter_attributes = [] + private - attributes = sanitize_for_mass_assignment(attributes) + def _assign_attributes(attributes) # :nodoc: + multi_parameter_attributes = {} + nested_parameter_attributes = {} attributes.each do |k, v| if k.include?("(") - multi_parameter_attributes << [ k, v ] + multi_parameter_attributes[k] = attributes.delete(k) elsif v.is_a?(Hash) - nested_parameter_attributes << [ k, v ] - else - _assign_attribute(k, v) + nested_parameter_attributes[k] = attributes.delete(k) end end + super(attributes) assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? end - alias attributes= assign_attributes - - private - - def _assign_attribute(k, v) - public_send("#{k}=", v) - rescue NoMethodError - if respond_to?("#{k}=") - raise - else - raise UnknownAttributeError.new(self, k) - end - end - # Assign any deferred nested attributes after the base attributes have been set. def assign_nested_parameter_attributes(pairs) pairs.each { |k, v| _assign_attribute(k, v) } @@ -81,13 +50,18 @@ module ActiveRecord errors = [] callstack.each do |name, values_with_empty_parameters| begin - send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value) + if values_with_empty_parameters.each_value.all?(&:nil?) + values = nil + else + values = values_with_empty_parameters + end + send("#{name}=", values) rescue => ex errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end end unless errors.empty? - error_descriptions = errors.map { |ex| ex.message }.join(",") + error_descriptions = errors.map(&:message).join(",") raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]" end end @@ -113,100 +87,5 @@ module ActiveRecord def find_parameter_position(multiparameter_name) multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i end - - class MultiparameterAttribute #:nodoc: - attr_reader :object, :name, :values, :column - - def initialize(object, name, values) - @object = object - @name = name - @values = values - end - - def read_value - return if values.values.compact.empty? - - @column = object.column_for_attribute(name) - klass = column ? column.klass : nil - - if klass == Time - read_time - elsif klass == Date - read_date - else - read_other(klass) - end - end - - private - - def instantiate_time_object(set_values) - if object.class.send(:create_time_zone_conversion_attribute?, name, column) - Time.zone.local(*set_values) - else - Time.send(object.class.default_timezone, *set_values) - end - end - - def read_time - # If column is a :time (and not :date or :datetime) there is no need to validate if - # there are year/month/day fields - if column.type == :time - # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil - { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| - values[key] ||= value - end - else - # else column is a timestamp, so if Date bits were not provided, error - validate_required_parameters!([1,2,3]) - - # If Date bits were provided but blank, then return nil - return if blank_date_parameter? - end - - max_position = extract_max_param(6) - set_values = values.values_at(*(1..max_position)) - # If Time bits are not there, then default to 0 - (3..5).each { |i| set_values[i] = set_values[i].presence || 0 } - instantiate_time_object(set_values) - end - - def read_date - return if blank_date_parameter? - set_values = values.values_at(1,2,3) - begin - Date.new(*set_values) - rescue ArgumentError # if Date.new raises an exception on an invalid date - instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates - end - end - - def read_other(klass) - max_position = extract_max_param - positions = (1..max_position) - validate_required_parameters!(positions) - - values.slice(*positions) - end - - # Checks whether some blank date parameter exists. Note that this is different - # than the validate_required_parameters! method, since it just checks for blank - # positions instead of missing ones, and does not raise in case one blank position - # exists. The caller is responsible to handle the case of this returning true. - def blank_date_parameter? - (1..3).any? { |position| values[position].blank? } - end - - # If some position is not provided, it errors out a missing parameter exception. - def validate_required_parameters!(positions) - if missing_parameter = positions.detect { |position| !values.key?(position) } - raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})") - end - end - - def extract_max_param(upper_cap = 100) - [values.keys.max, upper_cap].min - end - end end end diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 596161f81d..7d0ae32411 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -4,29 +4,62 @@ module ActiveRecord included do class_attribute :attribute_type_decorations, instance_accessor: false # :internal: - self.attribute_type_decorations = Hash.new({}) + self.attribute_type_decorations = TypeDecorator.new end - module ClassMethods + module ClassMethods # :nodoc: def decorate_attribute_type(column_name, decorator_name, &block) - clear_caches_calculated_from_columns - column_name = column_name.to_s + matcher = ->(name, _) { name == column_name.to_s } + key = "_#{column_name}_#{decorator_name}" + decorate_matching_attribute_types(matcher, key, &block) + end + + def decorate_matching_attribute_types(matcher, decorator_name, &block) + reload_schema_from_cache + decorator_name = decorator_name.to_s # Create new hashes so we don't modify parent classes - decorations_for_column = attribute_type_decorations[column_name] - new_decorations = decorations_for_column.merge(decorator_name.to_s => block) - self.attribute_type_decorations = attribute_type_decorations.merge(column_name => new_decorations) + self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block]) + end + + private + + def load_schema! + super + attribute_types.each do |name, type| + decorated_type = attribute_type_decorations.apply(name, type) + define_attribute(name, decorated_type) + end + end + end + + class TypeDecorator # :nodoc: + delegate :clear, to: :@decorations + + def initialize(decorations = {}) + @decorations = decorations + end + + def merge(*args) + TypeDecorator.new(@decorations.merge(*args)) + end + + def apply(name, type) + decorations = decorators_for(name, type) + decorations.inject(type) do |new_type, block| + block.call(new_type) + end end private - def add_user_provided_columns(*) - super.map do |column| - decorations = attribute_type_decorations[column.name].values - decorated_type = decorations.inject(column.cast_type) do |type, block| - block.call(type) - end - column.with_type(decorated_type) + def decorators_for(name, type) + matching(name, type).map(&:last) + end + + def matching(name, type) + @decorations.values.select do |(matcher, _)| + matcher.call(name, type) end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ddccb29d09..9d58a19304 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/enumerable' +require 'active_support/core_ext/string/filters' require 'mutex_m' require 'thread_safe' @@ -31,7 +32,7 @@ module ActiveRecord end } - BLACKLISTED_CLASS_METHODS = %w(private public protected) + BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) class AttributeMethodCache def initialize @@ -57,6 +58,8 @@ module ActiveRecord end end + class GeneratedAttributeMethods < Module; end # :nodoc: + module ClassMethods def inherited(child_class) #:nodoc: child_class.initialize_generated_modules @@ -64,21 +67,23 @@ module ActiveRecord end def initialize_generated_modules # :nodoc: - @generated_attribute_methods = Module.new { extend Mutex_m } + @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m } @attribute_methods_generated = false include @generated_attribute_methods + + super end # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods # :nodoc: return false if @attribute_methods_generated - # Use a mutex; we don't want two thread simultaneously trying to define + # Use a mutex; we don't want two threads simultaneously trying to define # attribute methods. generated_attribute_methods.synchronize do return false if @attribute_methods_generated superclass.define_attribute_methods unless self == base_class - super(column_names) + super(attribute_names) @attribute_methods_generated = true end true @@ -86,7 +91,7 @@ module ActiveRecord def undefine_attribute_methods # :nodoc: generated_attribute_methods.synchronize do - super if @attribute_methods_generated + super if defined?(@attribute_methods_generated) && @attribute_methods_generated @attribute_methods_generated = false end end @@ -107,16 +112,17 @@ module ActiveRecord # # => false def instance_method_already_implemented?(method_name) if dangerous_attribute_method?(method_name) - raise DangerousAttributeError, "#{method_name} is defined by Active Record" + raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name." end if superclass == Base super else - # If B < A and A defines its own attribute method, then we don't want to overwrite that. - defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods) - base_defined = Base.method_defined?(method_name) || Base.private_method_defined?(method_name) - defined && !base_defined || super + # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass + # defines its own attribute method, then we don't want to overwrite that. + defined = method_defined_within?(method_name, superclass, Base) && + ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods) + defined || super end end @@ -144,7 +150,7 @@ module ActiveRecord BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) end - def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc + def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: if klass.respond_to?(name, true) if superklass.respond_to?(name, true) klass.method(name).owner != superklass.method(name).owner @@ -156,16 +162,6 @@ module ActiveRecord end end - def find_generated_attribute_method(method_name) # :nodoc: - klass = self - until klass == Base - gen_methods = klass.generated_attribute_methods - return gen_methods.instance_method(method_name) if method_defined_within?(method_name, gen_methods, Object) - klass = klass.superclass - end - nil - end - # Returns +true+ if +attribute+ is an attribute method and table exists, # +false+ otherwise. # @@ -189,7 +185,7 @@ module ActiveRecord # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names @attribute_names ||= if !abstract_class? && table_exists? - column_names + attribute_types.keys else [] end @@ -204,7 +200,7 @@ module ActiveRecord # # person = Person.new # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter - # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> + # # => #<ActiveRecord::ConnectionAdapters::Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> # # person.column_for_attribute(:nothing) # # => #<ActiveRecord::ConnectionAdapters::NullColumn:0xXXX @name=nil, @sql_type=nil, @cast_type=#<Type::Value>, ...> @@ -216,28 +212,9 @@ module ActiveRecord end end - # If we haven't generated any methods yet, generate them, then - # see if we've created the method we're looking for. - def method_missing(method, *args, &block) # :nodoc: - self.class.define_attribute_methods - if respond_to_without_attributes?(method) - # make sure to invoke the correct attribute method, as we might have gotten here via a `super` - # call in a overwritten attribute method - if attribute_method = self.class.find_generated_attribute_method(method) - # this is probably horribly slow, but should only happen at most once for a given AR class - attribute_method.bind(self).call(*args, &block) - else - return super unless respond_to_missing?(method, true) - send(method, *args, &block) - end - else - super - end - end - # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> - # which will all return +true+. It also define the attribute methods if they have + # which will all return +true+. It also defines the attribute methods if they have # not been generated. # # class Person < ActiveRecord::Base @@ -252,18 +229,14 @@ module ActiveRecord # person.respond_to('age?') # => true # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) + return false unless super name = name.to_s - self.class.define_attribute_methods - result = super - - # If the result is false the answer is false. - return false unless result # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. # We check defined?(@attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. - if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name) + if defined?(@attributes) && self.class.column_names.include?(name) return has_attribute?(name) end @@ -280,7 +253,7 @@ module ActiveRecord # person.has_attribute?('age') # => true # person.has_attribute?(:nothing) # => false def has_attribute?(attr_name) - @attributes.has_key?(attr_name.to_s) + @attributes.key?(attr_name.to_s) end # Returns an array of names for the attributes available on this object. @@ -304,15 +277,13 @@ module ActiveRecord # person.attributes # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22} def attributes - attribute_names.each_with_object({}) { |name, attrs| - attrs[name] = read_attribute(name) - } + @attributes.to_hash end # Returns an <tt>#inspect</tt>-like string for the value of the - # attribute +attr_name+. String attributes are truncated upto 50 + # attribute +attr_name+. String attributes are truncated up to 50 # characters, Date and Time attributes are returned in the - # <tt>:db</tt> format, Array attributes are truncated upto 10 values. + # <tt>:db</tt> format, Array attributes are truncated up to 10 values. # Other attributes return the value of <tt>#inspect</tt> without # modification. # @@ -357,7 +328,7 @@ module ActiveRecord # task.attribute_present?(:title) # => true # task.attribute_present?(:is_done) # => true def attribute_present?(attribute) - value = read_attribute(attribute) + value = _read_attribute(attribute) !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end @@ -398,14 +369,41 @@ module ActiveRecord write_attribute(attr_name, value) end - protected - - def clone_attributes # :nodoc: - @attributes.each_with_object({}) do |(name, attr), h| - h[name] = attr.dup - end + # Returns the name of all database fields which have been read from this + # model. This can be useful in development mode to determine which fields + # need to be selected. For performance critical pages, selecting only the + # required fields can be an easy performance win (assuming you aren't using + # all of the fields on the model). + # + # For example: + # + # class PostsController < ActionController::Base + # after_action :print_accessed_fields, only: :index + # + # def index + # @posts = Post.all + # end + # + # private + # + # def print_accessed_fields + # p @posts.first.accessed_fields + # end + # end + # + # Which allows you to quickly change your code to: + # + # class PostsController < ActionController::Base + # def index + # @posts = Post.select(:id, :title, :author_id, :updated_at) + # end + # end + def accessed_fields + @attributes.accessed end + protected + def clone_attribute_value(reader_method, attribute_name) # :nodoc: value = send(reader_method, attribute_name) value.duplicable? ? value.clone : value @@ -423,7 +421,7 @@ module ActiveRecord def attribute_method?(attr_name) # :nodoc: # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@attributes) && @attributes.include?(attr_name) + defined?(@attributes) && @attributes.key?(attr_name) end private @@ -464,7 +462,7 @@ module ActiveRecord end def typecasted_attribute_value(name) - read_attribute(name) + _read_attribute(name) end end end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index d057f0941a..56c1898551 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -28,6 +28,7 @@ module ActiveRecord included do attribute_method_suffix "_before_type_cast" + attribute_method_suffix "_came_from_user?" end # Returns the value of the attribute identified by +attr_name+ before @@ -43,9 +44,7 @@ module ActiveRecord # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) - if attr = @attributes[attr_name.to_s] - attr.value_before_type_cast - end + @attributes[attr_name.to_s].value_before_type_cast end # Returns a hash of attributes before typecasting and deserialization. @@ -59,7 +58,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast - @attributes.each_with_object({}) { |(k, v), h| h[k] = v.value_before_type_cast } + @attributes.values_before_type_cast end private @@ -68,6 +67,10 @@ module ActiveRecord def attribute_before_type_cast(attribute_name) read_attribute_before_type_cast(attribute_name) end + + def attribute_came_from_user?(attribute_name) + @attributes[attribute_name].came_from_user? + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 6a5c057384..7ba907f786 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -34,55 +34,56 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - reset_changes + clear_changes_information end end def initialize_dup(other) # :nodoc: super - init_changed_attributes + calculate_changes_from_defaults end - def changed? - super || changed_in_place.any? + def changes_applied + super + store_original_raw_attributes end - def changed - super | changed_in_place + def clear_changes_information + super + original_raw_attributes.clear end - def attribute_changed?(attr_name, options = {}) - result = super - # We can't change "from" something in place. Only setters can define - # "from" and "to" - result ||= changed_in_place?(attr_name) unless options.key?(:from) - result + def changed_attributes + # This should only be set by methods which will call changed_attributes + # multiple times when it is known that the computed value cannot change. + if defined?(@cached_changed_attributes) + @cached_changed_attributes + else + super.reverse_merge(attributes_changed_in_place).freeze + end end - def changes_applied - super - store_original_raw_attributes + def changes + cache_changed_attributes do + super + end end - def reset_changes - super - original_raw_attributes.clear + def attribute_changed_in_place?(attr_name) + old_value = original_raw_attribute(attr_name) + @attributes[attr_name].changed_in_place_from?(old_value) end private - def initialize_internals_callback - super - init_changed_attributes + def changes_include?(attr_name) + super || attribute_changed_in_place?(attr_name) end - def init_changed_attributes + def calculate_changes_from_defaults @changed_attributes = nil - # Intentionally avoid using #column_defaults since overridden defaults (as is done in - # optimistic locking) won't get written unless they get marked as changed - self.class.columns.each do |c| - attr, orig_value = c.name, c.default - changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value) + self.class.column_defaults.each do |attr, orig_value| + set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value) end end @@ -107,10 +108,10 @@ module ActiveRecord end def save_changed_attribute(attr, old_value) - if attribute_changed?(attr) - changed_attributes.delete(attr) unless _field_changed?(attr, old_value) + if attribute_changed_by_setter?(attr) + clear_attribute_changes(attr) unless _field_changed?(attr, old_value) else - changed_attributes[attr] = old_value if _field_changed?(attr, old_value) + set_attribute_was(attr, old_value) if _field_changed?(attr, old_value) end end @@ -118,7 +119,7 @@ module ActiveRecord if attribute_changed?(attr) changed_attributes[attr] else - clone_attribute_value(:read_attribute, attr) + clone_attribute_value(:_read_attribute, attr) end end @@ -130,29 +131,25 @@ module ActiveRecord partial_writes? ? super(keys_for_partial_write) : super end - # Serialized attributes should always be written in case they've been - # changed in place. def keys_for_partial_write - changed + changed & self.class.column_names end def _field_changed?(attr, old_value) - new_value = read_attribute(attr) - raw_value = read_attribute_before_type_cast(attr) - column_for_attribute(attr).changed?(old_value, new_value, raw_value) + @attributes[attr].changed_from?(old_value) end - def changed_in_place - self.class.attribute_names.select do |attr_name| - changed_in_place?(attr_name) + def attributes_changed_in_place + changed_in_place.each_with_object({}) do |attr_name, h| + orig = @attributes[attr_name].original_value + h[attr_name] = orig end end - def changed_in_place?(attr_name) - type = type_for_attribute(attr_name) - old_value = original_raw_attribute(attr_name) - value = read_attribute(attr_name) - type.changed_in_place?(old_value, value) + def changed_in_place + self.class.attribute_names.select do |attr_name| + attribute_changed_in_place?(attr_name) + end end def original_raw_attribute(attr_name) @@ -166,9 +163,7 @@ module ActiveRecord end def store_original_raw_attribute(attr_name) - type = type_for_attribute(attr_name) - value = type.type_cast_for_database(read_attribute(attr_name)) - original_raw_attributes[attr_name] = value + original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil end def store_original_raw_attributes @@ -176,6 +171,13 @@ module ActiveRecord store_original_raw_attribute(attr) end end + + def cache_changed_attributes + @cached_changed_attributes = changed_attributes + yield + ensure + remove_instance_variable(:@cached_changed_attributes) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 1c81a5b71b..c28374e4ab 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -17,7 +17,7 @@ module ActiveRecord def id if pk = self.class.primary_key sync_with_transaction_state - read_attribute(pk) + _read_attribute(pk) end end @@ -39,6 +39,12 @@ module ActiveRecord read_attribute_before_type_cast(self.class.primary_key) end + # Returns the primary key previous value. + def id_was + sync_with_transaction_state + attribute_was(self.class.primary_key) + end + protected def attribute_method?(attr_name) @@ -54,7 +60,7 @@ module ActiveRecord end end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast).to_set + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set def dangerous_attribute_method?(method_name) super && !ID_ATTRIBUTE_METHODS.include?(method_name) @@ -83,12 +89,9 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: - return 'id' if base_name.blank? - - case primary_key_prefix_type - when :table_name + if base_name && primary_key_prefix_type == :table_name base_name.foreign_key(false) - when :table_name_with_underscore + elsif base_name && primary_key_prefix_type == :table_name_with_underscore base_name.foreign_key else if ActiveRecord::Base != self && table_exists? @@ -117,6 +120,7 @@ module ActiveRecord def primary_key=(value) @primary_key = value && value.to_s @quoted_primary_key = nil + @attributes_builder = nil end end end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 0f9723febb..553122a5fc 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -8,7 +8,7 @@ module ActiveRecord end def query_attribute(attr_name) - value = read_attribute(attr_name) { |n| missing_attribute(n, caller) } + value = self[attr_name] case value when true then true @@ -22,7 +22,7 @@ module ActiveRecord return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) !value.blank? end - elsif column.number? + elsif value.respond_to?(:zero?) !value.zero? else !value.blank? diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 8c1cc128f7..0d989c2eca 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/module/method_transplanting' - module ActiveRecord module AttributeMethods module Read @@ -27,7 +25,7 @@ module ActiveRecord <<-EOMETHOD def #{method_name} name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - read_attribute(name) { |n| missing_attribute(n, caller) } + _read_attribute(name) { |n| missing_attribute(n, caller) } end EOMETHOD end @@ -36,69 +34,56 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name| - define_method method_name do |*| - cached_attributes_deprecation_warning(method_name) - true - end - end - protected - def cached_attributes_deprecation_warning(method_name) - ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) - Calling `#{method_name}` is no longer necessary. All attributes are cached. - MESSAGE - end - - if Module.methods_transplantable? - def define_method_attribute(name) - method = ReaderMethodCache[name] - generated_attribute_methods.module_eval { define_method name, method } - end - else - def define_method_attribute(name) - safe_name = name.unpack('h*').first - temp_method = "__temp__#{safe_name}" - - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + def define_method_attribute(name) + safe_name = name.unpack('h*').first + temp_method = "__temp__#{safe_name}" - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def #{temp_method} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - read_attribute(name) { |n| missing_attribute(n, caller) } - end - STR + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - generated_attribute_methods.module_eval do - alias_method name, temp_method - undef_method temp_method + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def #{temp_method} + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + _read_attribute(name) { |n| missing_attribute(n, caller) } end + STR + + generated_attribute_methods.module_eval do + alias_method name, temp_method + undef_method temp_method end end end + ID = 'id'.freeze + # Returns the value of the attribute identified by <tt>attr_name</tt> after # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). - def read_attribute(attr_name) + def read_attribute(attr_name, &block) name = attr_name.to_s - @attributes.fetch(name) { - if name == 'id' - return read_attribute(self.class.primary_key) - elsif block_given? && self.class.columns_hash.key?(name) - return yield(name) - else - return nil - end - }.value + name = self.class.primary_key if name == ID + _read_attribute(name, &block) end - private - - def attribute(attribute_name) - read_attribute(attribute_name) + # This method exists to avoid the expensive primary_key check internally, without + # breaking compatibility with the read_attribute API + if defined?(JRUBY_VERSION) + # This form is significantly faster on JRuby, and this is one of our biggest hotspots. + # https://github.com/jruby/jruby/pull/2562 + def _read_attribute(attr_name, &block) # :nodoc + @attributes.fetch_value(attr_name.to_s, &block) + end + else + def _read_attribute(attr_name) # :nodoc: + @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? } + end end + + alias :attribute :_read_attribute + private :attribute + end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index cec50f62a3..d0d8a968c5 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -3,29 +3,13 @@ module ActiveRecord module Serialization extend ActiveSupport::Concern - included do - # Returns a hash of all the attributes that have been specified for - # serialization as keys and their class restriction as values. - class_attribute :serialized_attributes, instance_accessor: false - self.serialized_attributes = {} - 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 # serialization is done through YAML. If +class_name+ is specified, the - # serialized object must be of that class on retrieval or - # <tt>SerializationTypeMismatch</tt> will be raised. - # - # A notable side effect of serialized attributes is that the model will - # be updated on every save, even if it is not dirty. + # serialized object must be of that class on assignment and retrieval. + # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. # # ==== Parameters # @@ -50,7 +34,12 @@ module ActiveRecord # serialize :preferences, Hash # end def serialize(attr_name, class_name_or_coder = Object) - coder = if [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } + # When ::JSON is used, force it to go through the Active Support JSON encoder + # to ensure special objects (e.g. Active Record models) are dumped correctly + # using the #as_json hook. + coder = if class_name_or_coder == ::JSON + Coders::JSON + elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } class_name_or_coder else Coders::YAMLColumn.new(class_name_or_coder) @@ -59,10 +48,6 @@ module ActiveRecord decorate_attribute_type(attr_name, :serialize) do |type| Type::Serialized.new(type, coder) end - - # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy - # has its own hash of own serialized attributes - self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index abad949ef4..f9beb43e4b 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,19 +1,27 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion - class Type < SimpleDelegator # :nodoc: - def type_cast_from_database(value) + class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc: + def deserialize(value) convert_time_to_time_zone(super) end - def type_cast_from_user(value) + def cast(value) if value.is_a?(Array) - value.map { |v| type_cast_from_user(v) } + value.map { |v| cast(v) } + elsif value.is_a?(Hash) + set_time_zone_without_conversion(super) elsif value.respond_to?(:in_time_zone) - value.in_time_zone + begin + user_input_in_time_zone(value) || super + rescue ArgumentError + nil + end end end + private + def convert_time_to_time_zone(value) if value.is_a?(Array) value.map { |v| convert_time_to_time_zone(v) } @@ -23,6 +31,10 @@ module ActiveRecord value end end + + def set_time_zone_without_conversion(value) + ::Time.zone.local_to_utc(value).in_time_zone + end end extend ActiveSupport::Concern @@ -33,14 +45,54 @@ module ActiveRecord class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false self.skip_time_zone_conversion_for_attributes = [] + + class_attribute :time_zone_aware_types, instance_writer: false + self.time_zone_aware_types = [:datetime, :not_explicitly_configured] end module ClassMethods private - def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && - !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && - (:datetime == column.type) + + def inherited(subclass) + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or + # `skip_time_zone_conversion_for_attributes` would not be picked up. + subclass.class_eval do + matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } + decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| + TimeZoneConverter.new(type) + end + end + super + end + + def create_time_zone_conversion_attribute?(name, cast_type) + enabled_for_column = time_zone_aware_attributes && + !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) + result = enabled_for_column && + time_zone_aware_types.include?(cast_type.type) + + if enabled_for_column && + !result && + cast_type.type == :time && + time_zone_aware_types.include?(:not_explicitly_configured) + ActiveSupport::Deprecation.warn(<<-MESSAGE) + Time columns will become time zone aware in Rails 5.1. This + still causes `String`s to be parsed as if they were in `Time.zone`, + and `Time`s to be converted to `Time.zone`. + + To keep the old behavior, you must add the following to your initializer: + + config.active_record.time_zone_aware_types = [:datetime] + + To silence this deprecation warning, add the following: + + config.active_record.time_zone_aware_types << :time + MESSAGE + end + + result end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 246a2cd8ba..ab017c7b54 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/module/method_transplanting' - module ActiveRecord module AttributeMethods module Write @@ -25,27 +23,18 @@ module ActiveRecord module ClassMethods protected - if Module.methods_transplantable? - def define_method_attribute=(name) - method = WriterMethodCache[name] - generated_attribute_methods.module_eval { - define_method "#{name}=", method - } - end - else - def define_method_attribute=(name) - safe_name = name.unpack('h*').first - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + def define_method_attribute=(name) + safe_name = name.unpack('h*').first + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name}=(value) - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - write_attribute(name, value) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR - end + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__#{safe_name}=(value) + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + write_attribute(name, value) + end + alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= + undef_method :__temp__#{safe_name}= + STR end end @@ -69,16 +58,11 @@ module ActiveRecord def write_attribute_with_type_cast(attr_name, value, should_type_cast) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - type = type_for_attribute(attr_name) - - unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name) - raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" - end if should_type_cast - @attributes[attr_name] = Attribute.from_user(value, type) + @attributes.write_from_user(attr_name, value) else - @attributes[attr_name] = Attribute.from_database(value, type) + @attributes.write_cast_value(attr_name, value) end value diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb new file mode 100644 index 0000000000..013a7d0e01 --- /dev/null +++ b/activerecord/lib/active_record/attribute_set.rb @@ -0,0 +1,93 @@ +require 'active_record/attribute_set/builder' + +module ActiveRecord + class AttributeSet # :nodoc: + def initialize(attributes) + @attributes = attributes + end + + def [](name) + attributes[name] || Attribute.null(name) + end + + def []=(name, value) + attributes[name] = value + end + + def values_before_type_cast + attributes.transform_values(&:value_before_type_cast) + end + + def to_hash + initialized_attributes.transform_values(&:value) + end + alias_method :to_h, :to_hash + + def key?(name) + attributes.key?(name) && self[name].initialized? + end + + def keys + attributes.each_key.select { |name| self[name].initialized? } + end + + if defined?(JRUBY_VERSION) + # This form is significantly faster on JRuby, and this is one of our biggest hotspots. + # https://github.com/jruby/jruby/pull/2562 + def fetch_value(name, &block) + self[name].value(&block) + end + else + def fetch_value(name) + self[name].value { |n| yield n if block_given? } + end + end + + def write_from_database(name, value) + attributes[name] = self[name].with_value_from_database(value) + end + + def write_from_user(name, value) + attributes[name] = self[name].with_value_from_user(value) + end + + def write_cast_value(name, value) + attributes[name] = self[name].with_cast_value(value) + end + + def freeze + @attributes.freeze + super + end + + def initialize_dup(_) + @attributes = attributes.deep_dup + super + end + + def initialize_clone(_) + @attributes = attributes.clone + super + end + + def reset(key) + if key?(key) + write_from_database(key, nil) + end + end + + def accessed + attributes.select { |_, attr| attr.has_been_read? }.keys + end + + protected + + attr_reader :attributes + + private + + def initialized_attributes + attributes.select { |_, attr| attr.initialized? } + end + end +end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb new file mode 100644 index 0000000000..e85777c335 --- /dev/null +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -0,0 +1,92 @@ +module ActiveRecord + class AttributeSet # :nodoc: + class Builder # :nodoc: + attr_reader :types, :always_initialized + + def initialize(types, always_initialized = nil) + @types = types + @always_initialized = always_initialized + end + + def build_from_database(values = {}, additional_types = {}) + if always_initialized && !values.key?(always_initialized) + values[always_initialized] = nil + end + + attributes = LazyAttributeHash.new(types, values, additional_types) + AttributeSet.new(attributes) + end + end + end + + class LazyAttributeHash # :nodoc: + delegate :transform_values, :each_key, to: :materialize + + def initialize(types, values, additional_types) + @types = types + @values = values + @additional_types = additional_types + @materialized = false + @delegate_hash = {} + end + + def key?(key) + delegate_hash.key?(key) || values.key?(key) || types.key?(key) + end + + def [](key) + delegate_hash[key] || assign_default_value(key) + end + + def []=(key, value) + if frozen? + raise RuntimeError, "Can't modify frozen hash" + end + delegate_hash[key] = value + end + + def initialize_dup(_) + @delegate_hash = delegate_hash.transform_values(&:dup) + super + end + + def select + keys = types.keys | values.keys | delegate_hash.keys + keys.each_with_object({}) do |key, hash| + attribute = self[key] + if yield(key, attribute) + hash[key] = attribute + end + end + end + + protected + + attr_reader :types, :values, :additional_types, :delegate_hash + + private + + def assign_default_value(name) + type = additional_types.fetch(name, types[name]) + value_present = true + value = values.fetch(name) { value_present = false } + + if value_present + delegate_hash[name] = Attribute.from_database(name, value, type) + elsif types.key?(name) + delegate_hash[name] = Attribute.uninitialized(name, type) + end + end + + def materialize + unless @materialized + values.each_key { |key| self[key] } + types.each_key { |key| self[key] } + unless frozen? + @materialized = true + end + end + delegate_hash + end + end +end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 9c80121d70..c8979a60d7 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -1,33 +1,46 @@ module ActiveRecord - module Attributes # :nodoc: + # See ActiveRecord::Attributes::ClassMethods for documentation + module Attributes extend ActiveSupport::Concern + # :nodoc: Type = ActiveRecord::Type included do - class_attribute :user_provided_columns, instance_accessor: false # :internal: - self.user_provided_columns = {} + class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: + self.attributes_to_define_after_schema_loads = {} end module ClassMethods - # Defines or overrides a attribute on this model. This allows customization of - # Active Record's type casting behavior, as well as adding support for user defined - # types. + # Defines an attribute with a type on this model. It will override the + # type of existing attributes if needed. This allows control over how + # values are converted to and from SQL when assigned to a model. It also + # changes the behavior of values passed to + # ActiveRecord::QueryMethods#where. This will let you use + # your domain objects across much of Active Record, without having to + # rely on implementation details or monkey patching. # - # +name+ The name of the methods to define attribute methods for, and the column which - # this will persist to. + # +name+ The name of the methods to define attribute methods for, and the + # column which this will persist to. # - # +cast_type+ A type object that contains information about how to type cast the value. - # See the examples section for more information. + # +cast_type+ A symbol such as +:string+ or +:integer+, or a type object + # to be used for this attribute. See the examples below for more + # information about providing custom type objects. # # ==== Options - # The options hash accepts the following options: + # The following options are accepted: # - # +default+ is the default value that the column should use on a new record. + # +default+ The default value to use when no value is provided. If this option + # is not passed, the previous default value (if any) will be used. + # Otherwise, the default will be +nil+. + # + # +array+ (PG only) specifies that the type should be an array (see the examples below). + # + # +range+ (PG only) specifies that the type should be a range (see the examples below). # # ==== Examples # - # The type detected by Active Record can be overriden. + # The type detected by Active Record can be overridden. # # # db/schema.rb # create_table :store_listings, force: true do |t| @@ -44,78 +57,188 @@ module ActiveRecord # store_listing.price_in_cents # => BigDecimal.new(10.1) # # class StoreListing < ActiveRecord::Base - # attribute :price_in_cents, Type::Integer.new + # attribute :price_in_cents, :integer # end # # # after # store_listing.price_in_cents # => 10 # - # Users may also define their own custom types, as long as they respond to the methods - # defined on the value type. The `type_cast` method on your type object will be called - # with values both from the database, and from your controllers. See - # `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your - # type objects inherit from an existing type, or the base value type. + # A default can also be provided. + # + # create_table :store_listings, force: true do |t| + # t.string :my_string, default: "original default" + # end + # + # StoreListing.new.my_string # => "original default" + # + # class StoreListing < ActiveRecord::Base + # attribute :my_string, :string, default: "new default" + # end + # + # StoreListing.new.my_string # => "new default" + # + # Attributes do not need to be backed by a database column. + # + # class MyModel < ActiveRecord::Base + # attribute :my_string, :string + # attribute :my_int_array, :integer, array: true + # attribute :my_float_range, :float, range: true + # end + # + # model = MyModel.new( + # my_string: "string", + # my_int_array: ["1", "2", "3"], + # my_float_range: "[1,3.5]", + # ) + # model.attributes + # # => + # { + # my_string: "string", + # my_int_array: [1, 2, 3], + # my_float_range: 1.0..3.5 + # } + # + # ==== Creating Custom Types + # + # Users may also define their own custom types, as long as they respond + # to the methods defined on the value type. The method + # +deserialize+ or +cast+ will be called on + # your type object, with raw input from the database or from your + # controllers. See ActiveRecord::Type::Value for the expected API. It is + # recommended that your type objects inherit from an existing type, or + # from ActiveRecord::Type::Value # # class MoneyType < ActiveRecord::Type::Integer - # def type_cast(value) + # def cast(value) # if value.include?('$') # price_in_dollars = value.gsub(/\$/, '').to_f - # price_in_dollars * 100 + # super(price_in_dollars * 100) # else - # value.to_i + # super # end # end # end # + # # config/initializers/types.rb + # ActiveRecord::Type.register(:money, MoneyType) + # + # # /app/models/store_listing.rb # class StoreListing < ActiveRecord::Base - # attribute :price_in_cents, MoneyType.new + # attribute :price_in_cents, :money # end # # store_listing = StoreListing.new(price_in_cents: '$10.00') # store_listing.price_in_cents # => 1000 - def attribute(name, cast_type, options = {}) + # + # For more details on creating custom types, see the documentation for + # ActiveRecord::Type::Value. For more details on registering your types + # to be referenced by a symbol, see ActiveRecord::Type.register. You can + # also pass a type object directly, in place of a symbol. + # + # ==== Querying + # + # When ActiveRecord::QueryMethods#where is called, it will + # use the type defined by the model class to convert the value to SQL, + # calling +serialize+ on your type object. For example: + # + # class Money < Struct.new(:amount, :currency) + # end + # + # class MoneyType < Type::Value + # def initialize(currency_converter) + # @currency_converter = currency_converter + # end + # + # # value will be the result of +deserialize+ or + # # +cast+. Assumed to be in instance of +Money+ in + # # this case. + # def serialize(value) + # value_in_bitcoins = @currency_converter.convert_to_bitcoins(value) + # value_in_bitcoins.amount + # end + # end + # + # ActiveRecord::Type.register(:money, MoneyType) + # + # class Product < ActiveRecord::Base + # currency_converter = ConversionRatesFromTheInternet.new + # attribute :price_in_bitcoins, :money, currency_converter + # end + # + # Product.where(price_in_bitcoins: Money.new(5, "USD")) + # # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230 + # + # Product.where(price_in_bitcoins: Money.new(5, "GBP")) + # # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412 + # + # ==== Dirty Tracking + # + # The type of an attribute is given the opportunity to change how dirty + # tracking is performed. The methods +changed?+ and +changed_in_place?+ + # will be called from ActiveModel::Dirty. See the documentation for those + # methods in ActiveRecord::Type::Value for more details. + def attribute(name, cast_type, **options) name = name.to_s - clear_caches_calculated_from_columns - # Assign a new hash to ensure that subclasses do not share a hash - self.user_provided_columns = user_provided_columns.merge(name => connection.new_column(name, options[:default], cast_type)) - end + reload_schema_from_cache - # Returns an array of column objects for the table associated with this class. - def columns - @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name)) + self.attributes_to_define_after_schema_loads = + attributes_to_define_after_schema_loads.merge( + name => [cast_type, options] + ) end - # Returns a hash of column objects for the table associated with this class. - def columns_hash - @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + # This is the low level API which sits beneath +attribute+. It only + # accepts type objects, and will do its work immediately instead of + # waiting for the schema to load. Automatic schema detection and + # ClassMethods#attribute both call this under the hood. While this method + # is provided so it can be used by plugin authors, application code + # should probably use ClassMethods#attribute. + # + # +name+ The name of the attribute being defined. Expected to be a +String+. + # + # +cast_type+ The type object to use for this attribute. + # + # +default+ The default value to use when no value is provided. If this option + # is not passed, the previous default value (if any) will be used. + # Otherwise, the default will be +nil+. + # + # +user_provided_default+ Whether the default value should be cast using + # +cast+ or +deserialize+. + def define_attribute( + name, + cast_type, + default: NO_DEFAULT_PROVIDED, + user_provided_default: true + ) + attribute_types[name] = cast_type + define_default_attribute(name, default, cast_type, from_user: user_provided_default) end - def reset_column_information # :nodoc: + def load_schema! # :nodoc: super - clear_caches_calculated_from_columns - end - - private + attributes_to_define_after_schema_loads.each do |name, (type, options)| + if type.is_a?(Symbol) + type = ActiveRecord::Type.lookup(type, **options.except(:default)) + end - def add_user_provided_columns(schema_columns) - existing_columns = schema_columns.map do |column| - user_provided_columns[column.name] || column + define_attribute(name, type, **options.slice(:default)) end + end - existing_column_names = existing_columns.map(&:name) - new_columns = user_provided_columns.except(*existing_column_names).values + private - existing_columns + new_columns - end + NO_DEFAULT_PROVIDED = Object.new # :nodoc: + private_constant :NO_DEFAULT_PROVIDED - def clear_caches_calculated_from_columns - @columns = nil - @columns_hash = nil - @column_types = nil - @column_defaults = nil - @raw_column_defaults = nil - @column_names = nil - @content_columns = nil + def define_default_attribute(name, value, type, from_user:) + if value == NO_DEFAULT_PROVIDED + default_attribute = _default_attributes[name].with_type(type) + elsif from_user + default_attribute = Attribute.from_user(name, value, type) + else + default_attribute = Attribute.from_database(name, value, type) + end + _default_attributes[name] = default_attribute end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index b3c3e26c9f..0792d19c3e 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -177,10 +177,8 @@ module ActiveRecord # before actually defining them. def add_autosave_association_callbacks(reflection) save_method = :"autosave_associated_records_for_#{reflection.name}" - validation_method = :"validate_associated_records_for_#{reflection.name}" - collection = reflection.collection? - if collection + if reflection.collection? before_save :before_save_collection_association define_non_cyclic_method(save_method) { save_collection_association(reflection) } @@ -200,13 +198,29 @@ module ActiveRecord after_create save_method after_update save_method else - define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } + define_non_cyclic_method(save_method) { throw(:abort) if save_belongs_to_association(reflection) == false } before_save save_method end + define_autosave_validation_callbacks(reflection) + end + + def define_autosave_validation_callbacks(reflection) + validation_method = :"validate_associated_records_for_#{reflection.name}" if reflection.validate? && !method_defined?(validation_method) - method = (collection ? :validate_collection_association : :validate_single_association) - define_non_cyclic_method(validation_method) { send(method, reflection) } + if reflection.collection? + method = :validate_collection_association + else + method = :validate_single_association + end + + define_non_cyclic_method(validation_method) do + send(method, reflection) + # TODO: remove the following line as soon as the return value of + # callbacks is ignored, that is, returning `false` does not + # display a deprecation warning or halts the callback chain. + true + end validate validation_method end end @@ -263,20 +277,27 @@ module ActiveRecord if new_record association && association.target elsif autosave - association.target.find_all { |record| record.changed_for_autosave? } + association.target.find_all(&:changed_for_autosave?) else - association.target.find_all { |record| record.new_record? } + association.target.find_all(&:new_record?) end end # go through nested autosave associations that are loaded in memory (without loading # any new ones), and return true if is changed for autosave def nested_records_changed_for_autosave? - self.class._reflections.values.any? do |reflection| - if reflection.options[:autosave] - association = association_instance_get(reflection.name) - association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + @_nested_records_changed_for_autosave_already_called ||= false + return false if @_nested_records_changed_for_autosave_already_called + begin + @_nested_records_changed_for_autosave_already_called = true + self.class._reflections.values.any? do |reflection| + if reflection.options[:autosave] + association = association_instance_get(reflection.name) + association && Array.wrap(association.target).any?(&:changed_for_autosave?) + end end + ensure + @_nested_records_changed_for_autosave_already_called = false end end @@ -340,7 +361,6 @@ module ActiveRecord autosave = reflection.options[:autosave] if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) - if autosave records_to_destroy = records.select(&:marked_for_destruction?) records_to_destroy.each { |record| association.destroy(record) } @@ -406,7 +426,9 @@ module ActiveRecord # If the record is new or it has changed, returns true. def record_changed?(reflection, record, key) - record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key) + record.new_record? || + (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) || + record.attribute_changed?(reflection.foreign_key) end # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index e4d0abb8ef..100d3780f6 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -9,6 +9,7 @@ require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/introspection' @@ -21,6 +22,7 @@ require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' require 'active_record/attributes' +require 'active_record/type_caster' module ActiveRecord #:nodoc: # = Active Record @@ -140,6 +142,7 @@ module ActiveRecord #:nodoc: # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. # Query methods allow you to test whether an attribute value is present. + # Additionally, when dealing with numeric values, a query method will return false if the value is zero. # # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call # to determine whether the user has a name: @@ -219,25 +222,9 @@ module ActiveRecord #:nodoc: # # == Single table inheritance # - # Active Record allows inheritance by storing the name of the class in a column that by - # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). - # This means that an inheritance looking like this: - # - # class Company < ActiveRecord::Base; end - # class Firm < Company; end - # class Client < Company; end - # class PriorityClient < Client; end - # - # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in - # the companies table with type = "Firm". You can then fetch this row again using - # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. - # - # If you don't have a type column defined in your table, single-table inheritance won't - # be triggered. In that case, it'll work just like normal subclasses with no special magic - # for differentiating between them or reloading the right type with find. - # - # Note, all the attributes for all the cases are kept in the same table. Read more: - # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # Active Record allows inheritance by storing the name of the class in a + # column that is named "type" by default. See ActiveRecord::Inheritance for + # more details. # # == Connection to multiple databases in different models # @@ -308,6 +295,8 @@ module ActiveRecord #:nodoc: include Integration include Validations include CounterCache + include Attributes + include AttributeDecorators include Locking::Optimistic include Locking::Pessimistic include AttributeMethods @@ -323,8 +312,7 @@ module ActiveRecord #:nodoc: include Reflection include Serialization include Store - include Attributes - include AttributeDecorators + include SecureToken end ActiveSupport.run_load_hooks(:active_record, Base) diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 5955673b42..f44e5af5de 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -192,14 +192,14 @@ module ActiveRecord # # == <tt>before_validation*</tt> returning statements # - # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be + # If the +before_validation+ callback throws +:abort+, the process will be # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a # ActiveRecord::RecordInvalid exception. Nothing will be appended to the errors object. # # == Canceling callbacks # - # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are - # cancelled. If an <tt>after_*</tt> callback returns +false+, all the later callbacks are cancelled. + # If a <tt>before_*</tt> callback throws +:abort+, all the later callbacks and + # the associated action are cancelled. # Callbacks are generally run in the order they are defined, with the exception of callbacks defined as # methods on the model, which are called last. # @@ -289,25 +289,25 @@ module ActiveRecord end def destroy #:nodoc: - run_callbacks(:destroy) { super } + _run_destroy_callbacks { super } end def touch(*) #:nodoc: - run_callbacks(:touch) { super } + _run_touch_callbacks { super } end private - def create_or_update #:nodoc: - run_callbacks(:save) { super } + def create_or_update(*) #:nodoc: + _run_save_callbacks { super } end def _create_record #:nodoc: - run_callbacks(:create) { super } + _run_create_callbacks { super } end def _update_record(*) #:nodoc: - run_callbacks(:update) { super } + _run_update_callbacks { super } end end end diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb new file mode 100644 index 0000000000..75d3bfe625 --- /dev/null +++ b/activerecord/lib/active_record/coders/json.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module Coders # :nodoc: + class JSON # :nodoc: + def self.dump(obj) + ActiveSupport::JSON.encode(obj) + end + + def self.load(json) + ActiveSupport::JSON.decode(json) unless json.nil? + end + end + end +end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index d3d7396c91..9ea22ed798 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -8,6 +8,7 @@ module ActiveRecord def initialize(object_class = Object) @object_class = object_class + check_arity_of_constructor end def dump(obj) @@ -33,6 +34,16 @@ module ActiveRecord obj end + + private + + def check_arity_of_constructor + begin + load(nil) + rescue ArgumentError + raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index db80c0faee..d99dc9a5db 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -121,13 +121,13 @@ module ActiveRecord # greater than the number of threads currently waiting (that # is, don't jump ahead in line). Otherwise, return nil. # - # If +timeout+ is given, block if it there is no element + # If +timeout+ is given, block if there is no element # available, waiting up to +timeout+ seconds for an element to # become available. # # Raises: # - ConnectionTimeoutError if +timeout+ is given and no element - # becomes available after +timeout+ seconds, + # becomes available within +timeout+ seconds, def poll(timeout = nil) synchronize do if timeout @@ -150,7 +150,7 @@ module ActiveRecord end # A thread can remove an element from the queue without - # waiting if an only if the number of currently available + # waiting if and only if the number of currently available # connections is strictly greater than the number of waiting # threads. def can_remove_no_wait? @@ -234,8 +234,8 @@ module ActiveRecord @spec = spec - @checkout_timeout = spec.config[:checkout_timeout] || 5 - @reaper = Reaper.new self, spec.config[:reaping_frequency] + @checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5 + @reaper = Reaper.new(self, (spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f)) @reaper.run # default max pool size to 5 @@ -319,9 +319,7 @@ module ActiveRecord checkin conn conn.disconnect! if conn.requires_reloading? end - @connections.delete_if do |conn| - conn.requires_reloading? - end + @connections.delete_if(&:requires_reloading?) @available.clear @connections.each do |conn| @available.add conn @@ -360,7 +358,7 @@ module ActiveRecord synchronize do owner = conn.owner - conn.run_callbacks :checkin do + conn._run_checkin_callbacks do conn.expire end @@ -428,7 +426,9 @@ module ActiveRecord def release(conn, owner) thread_id = owner.object_id - @reserved_connections.delete thread_id + if @reserved_connections[thread_id] == conn + @reserved_connections.delete thread_id + end end def new_connection @@ -449,10 +449,14 @@ module ActiveRecord end def checkout_and_verify(c) - c.run_callbacks :checkout do + c._run_checkout_callbacks do c.verify! end c + rescue + remove c + c.disconnect! + raise end end @@ -462,23 +466,44 @@ module ActiveRecord # # For example, suppose that you have 5 models, with the following hierarchy: # - # | - # +-- Book - # | | - # | +-- ScaryBook - # | +-- GoodBook - # +-- Author - # +-- BankAccount + # class Author < ActiveRecord::Base + # end + # + # class BankAccount < ActiveRecord::Base + # end + # + # class Book < ActiveRecord::Base + # establish_connection "library_db" + # end + # + # class ScaryBook < Book + # end + # + # class GoodBook < Book + # end # - # Suppose that Book is to connect to a separate database (i.e. one other - # than the default database). Then Book, ScaryBook and GoodBook will all use - # the same connection pool. Likewise, Author and BankAccount will use the - # same connection pool. However, the connection pool used by Author/BankAccount - # is not the same as the one used by Book/ScaryBook/GoodBook. + # And a database.yml that looked like this: # - # Normally there is only a single ConnectionHandler instance, accessible via - # ActiveRecord::Base.connection_handler. Active Record models use this to - # determine the connection pool that they should use. + # development: + # database: my_application + # host: localhost + # + # library_db: + # database: library + # host: some.library.org + # + # Your primary database in the development environment is "my_application" + # but the Book model connects to a separate database called "library_db" + # (this can even be a database on a different machine). + # + # Book, ScaryBook and GoodBook will all use the same connection pool to + # "library_db" while Author, BankAccount, and any other models you create + # will use the default connection pool to "my_application". + # + # The various connection pools are managed by a single instance of + # ConnectionHandler accessible via ActiveRecord::Base.connection_handler. + # All Active Record models use this handler to determine the connection pool that they + # should use. class ConnectionHandler def initialize # These caches are keyed by klass.name, NOT klass. Keying them by klass @@ -495,14 +520,7 @@ module ActiveRecord def connection_pool_list owner_to_pool.values.compact end - - def connection_pools - ActiveSupport::Deprecation.warn( - "In the next release, this will return the same as #connection_pool_list. " \ - "(An array of pools, rather than a hash mapping specs to pools.)" - ) - Hash[connection_pool_list.map { |pool| [pool.spec, pool] }] - end + alias :connection_pools :connection_pool_list def establish_connection(owner, spec) @class_to_pool.clear @@ -619,7 +637,7 @@ module ActiveRecord end def call(env) - testing = env.key?('rack.test') + testing = env['rack.test'] response = @app.call(env) response[2] = ::Rack::BodyProxy.new(response[2]) do diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 7ff5001796..42ad285340 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -83,6 +83,11 @@ module ActiveRecord exec_query(sql, name, binds) end + # Executes the truncate statement. + def truncate(table_name, name = nil) + raise NotImplementedError + end + # Executes update +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. @@ -193,72 +198,42 @@ module ActiveRecord # * You are creating a nested (savepoint) transaction # # The mysql, mysql2 and postgresql adapters support setting the transaction - # isolation level. However, support is disabled for mysql versions below 5, + # isolation level. However, support is disabled for MySQL versions below 5, # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] # which means the isolation level gets persisted outside the transaction. - def transaction(options = {}) - options.assert_valid_keys :requires_new, :joinable, :isolation - - if !options[:requires_new] && current_transaction.joinable? - if options[:isolation] + def transaction(requires_new: nil, isolation: nil, joinable: true) + if !requires_new && current_transaction.joinable? + if isolation raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" end - yield else - within_new_transaction(options) { yield } + transaction_manager.within_new_transaction(isolation: isolation, joinable: joinable) { yield } end rescue ActiveRecord::Rollback # rollbacks are silently swallowed end - def within_new_transaction(options = {}) #:nodoc: - transaction = begin_transaction(options) - yield - rescue Exception => error - rollback_transaction if transaction - raise - ensure - begin - commit_transaction unless error - rescue Exception - rollback_transaction - raise - end - end - - def open_transactions - @transaction.number - end + attr_reader :transaction_manager #:nodoc: - def current_transaction #:nodoc: - @transaction - end + delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager def transaction_open? - @transaction.open? - end - - def begin_transaction(options = {}) #:nodoc: - @transaction = @transaction.begin(options) - end - - def commit_transaction #:nodoc: - @transaction = @transaction.commit - end - - def rollback_transaction #:nodoc: - @transaction = @transaction.rollback + current_transaction.open? end def reset_transaction #:nodoc: - @transaction = ClosedTransaction.new(self) + @transaction_manager = TransactionManager.new(self) end # Register a record with the current transaction so that its after_commit and after_rollback callbacks # can be called. def add_transaction_record(record) - @transaction.add_record(record) + current_transaction.add_record(record) + end + + def transaction_state + current_transaction.state end # Begins the transaction (and turns off auto-committing). @@ -285,7 +260,18 @@ module ActiveRecord # Rolls back the transaction (and turns on auto-committing). Must be # done if the transaction block raises an exception or returns false. - def rollback_db_transaction() end + def rollback_db_transaction + exec_rollback_db_transaction + end + + def exec_rollback_db_transaction() end #:nodoc: + + def rollback_to_savepoint(name = nil) + exec_rollback_to_savepoint(name) + end + + def exec_rollback_to_savepoint(name = nil) #:nodoc: + end def default_sequence_name(table, column) nil @@ -301,10 +287,17 @@ module ActiveRecord def insert_fixture(fixture, table_name) columns = schema_cache.columns_hash(table_name) - key_list = [] - value_list = fixture.map do |name, value| - key_list << quote_column_name(name) - quote(value, columns[name]) + binds = fixture.map do |name, value| + type = lookup_cast_type_from_column(columns[name]) + Relation::QueryAttribute.new(name, value, type) + end + key_list = fixture.keys.map { |name| quote_column_name(name) } + value_list = prepare_binds_for_database(binds).map do |value| + begin + quote(value) + rescue TypeError + quote(YAML.dump(value)) + end end execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' @@ -314,10 +307,6 @@ module ActiveRecord "DEFAULT VALUES" end - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) - "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})" - end - # Sanitizes the given LIMIT parameter in order to prevent SQL injection. # # The +limit+ may be anything that can evaluate to a string via #to_s. It @@ -338,8 +327,8 @@ module ActiveRecord end # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work - # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in - # an UPDATE statement, so in the mysql adapters we redefine this to do that. + # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in + # an UPDATE statement, so in the MySQL adapters we redefine this to do that. def join_to_update(update, select) #:nodoc: key = update.key subselect = subquery_for(key, select) @@ -364,8 +353,9 @@ module ActiveRecord # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) + exec_query(sql, name, binds) end - undef_method :select + # Returns the last auto-generated ID from the affected table. def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) @@ -394,7 +384,7 @@ module ActiveRecord def binds_from_relation(relation, binds) if relation.is_a?(Relation) && binds.empty? - relation, binds = relation.arel, relation.bind_values + relation, binds = relation.arel, relation.bound_attributes end [relation, binds] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 4a4506c7f5..5e27cfe507 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -3,7 +3,7 @@ module ActiveRecord module QueryCache class << self def included(base) #:nodoc: - dirties_query_cache base, :insert, :update, :delete + dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction end def dirties_query_cache(base, *method_names) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 04ae67234f..d2840b9498 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -9,13 +9,14 @@ module ActiveRecord # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) - # FIXME: The only case we get an object other than nil or a real column - # is `SchemaStatements#add_column` with a PG array that has a non-empty default - # value. Is this really the only case? Are we missing tests for other types? - # We should have a real column object passed (or nil) here, and check for that - # instead - if column.respond_to?(:type_cast_for_database) - value = column.type_cast_for_database(value) + if column + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a column to `quote` has been deprecated. It is only used + for type casting, which should be handled elsewhere. See + https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11 + for more information. + MSG + value = type_cast_from_column(column, value) end _quote(value) @@ -24,18 +25,13 @@ module ActiveRecord # Cast a +value+ to a type that the database understands. For example, # SQLite does not understand dates, so this method will convert a Date # to a String. - def type_cast(value, column) + def type_cast(value, column = nil) if value.respond_to?(:quoted_id) && value.respond_to?(:id) return value.id end - # FIXME: The only case we get an object other than nil or a real column - # is `SchemaStatements#add_column` with a PG array that has a non-empty default - # value. Is this really the only case? Are we missing tests for other types? - # We should have a real column object passed (or nil) here, and check for that - # instead - if column.respond_to?(:type_cast_for_database) - value = column.type_cast_for_database(value) + if column + value = type_cast_from_column(column, value) end _type_cast(value) @@ -44,6 +40,40 @@ module ActiveRecord raise TypeError, "can't cast #{value.class}#{to_type}" end + # If you are having to call this function, you are likely doing something + # wrong. The column does not have sufficient type information if the user + # provided a custom type on the class level either explicitly (via + # `attribute`) or implicitly (via `serialize`, + # `time_zone_aware_attributes`). In almost all cases, the sql type should + # only be used to change quoting behavior, when the primitive to + # represent the type doesn't sufficiently reflect the differences + # (varchar vs binary) for example. The type used to get this primitive + # should have been provided before reaching the connection adapter. + def type_cast_from_column(column, value) # :nodoc: + if column + type = lookup_cast_type_from_column(column) + type.serialize(value) + else + value + end + end + + # See docs for +type_cast_from_column+ + def lookup_cast_type_from_column(column) # :nodoc: + lookup_cast_type(column.sql_type) + end + + def fetch_type_metadata(sql_type) + cast_type = lookup_cast_type(sql_type) + SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + end + # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) @@ -66,12 +96,17 @@ module ActiveRecord # This works for mysql and mysql2 where table.column can be used to # resolve ambiguity. # - # We override this in the sqlite and postgresql adapters to use only + # We override this in the sqlite3 and postgresql adapters to use only # the column name (as per syntax requirements). def quote_table_name_for_assignment(table, attr) quote_table_name("#{table}.#{attr}") end + def quote_default_expression(value, column) #:nodoc: + value = lookup_cast_type(column.sql_type).serialize(value) + quote(value) + end + def quoted_true "'t'" end @@ -97,7 +132,16 @@ module ActiveRecord end end - value.to_s(:db) + result = value.to_s(:db) + if value.respond_to?(:usec) && value.usec > 0 + "#{result}.#{sprintf("%06d", value.usec)}" + else + result + end + end + + def prepare_binds_for_database(binds) # :nodoc: + binds.map(&:value_for_database) end private @@ -118,9 +162,8 @@ module ActiveRecord when Numeric, ActiveSupport::Duration then value.to_s when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" - when Class then "'#{value.to_s}'" - else - "'#{quote_string(YAML.dump(value))}'" + when Class then "'#{value}'" + else raise TypeError, "can't quote #{value.class.name}" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb index 25c17ce971..c0662f8473 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb @@ -9,7 +9,7 @@ module ActiveRecord execute("SAVEPOINT #{name}") end - def rollback_to_savepoint(name = current_savepoint_name) + def exec_rollback_to_savepoint(name = current_savepoint_name) execute("ROLLBACK TO SAVEPOINT #{name}") end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 47fe501752..bc8fa9b6cf 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/strip' + module ActiveRecord module ConnectionAdapters class AbstractAdapter @@ -13,9 +15,7 @@ module ActiveRecord end def visit_AddColumn(o) - sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) - sql = "ADD #{quote_column_name(o.name)} #{sql_type}" - add_column_options!(sql, column_options(o)) + "ADD #{accept(o)}" end private @@ -23,12 +23,14 @@ module ActiveRecord def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') + sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') end def visit_ColumnDefinition(o) - sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) - column_sql = "#{quote_column_name(o.name)} #{sql_type}" - add_column_options!(column_sql, column_options(o)) unless o.primary_key? + o.sql_type ||= type_to_sql(o.type, o.limit, o.precision, o.scale) + column_sql = "#{quote_column_name(o.name)} #{o.sql_type}" + add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql end @@ -41,6 +43,21 @@ module ActiveRecord create_sql end + def visit_AddForeignKey(o) + sql = <<-SQL.strip_heredoc + ADD CONSTRAINT #{quote_column_name(o.name)} + FOREIGN KEY (#{quote_column_name(o.column)}) + REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) + SQL + sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete + sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update + sql + end + + def visit_DropForeignKey(name) + "DROP CONSTRAINT #{quote_column_name(name)}" + end + def column_options(o) column_options = {} column_options[:null] = o.null unless o.null.nil? @@ -48,6 +65,8 @@ module ActiveRecord column_options[:column] = o column_options[:first] = o.first column_options[:after] = o.after + column_options[:auto_increment] = o.auto_increment + column_options[:primary_key] = o.primary_key column_options end @@ -64,7 +83,7 @@ module ActiveRecord end def add_column_options!(sql, options) - sql << " DEFAULT #{quote_value(options[:default], options[:column])}" if options_include_default?(options) + sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options) # must explicitly check for :null to allow change_column to work on migrations if options[:null] == false sql << " NOT NULL" @@ -72,18 +91,32 @@ module ActiveRecord if options[:auto_increment] == true sql << " AUTO_INCREMENT" end + if options[:primary_key] == true + sql << " PRIMARY KEY" + end sql end - def quote_value(value, column) - column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale) - - @conn.quote(value, column) + def quote_default_expression(value, column) + @conn.quote_default_expression(value, column) end def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + + def action_sql(action, dependency) + case dependency + when :nullify then "ON #{action} SET NULL" + when :cascade then "ON #{action} CASCADE" + when :restrict then "ON #{action} RESTRICT" + else + raise ArgumentError, <<-MSG.strip_heredoc + '#{dependency}' is not supported for :on_update or :on_delete. + Supported values are: :nullify, :cascade, :restrict + MSG + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index a9b3e9cfb9..a2777fcd0a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,14 +15,131 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key end end - class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc: + class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: + end + + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: + def name + options[:name] + end + + def column + options[:column] + end + + def primary_key + options[:primary_key] || default_primary_key + end + + def on_delete + options[:on_delete] + end + + def on_update + options[:on_update] + end + + def custom_primary_key? + options[:primary_key] != default_primary_key + end + + def defined_for?(options_or_to_table = {}) + if options_or_to_table.is_a?(Hash) + options_or_to_table.all? {|key, value| options[key].to_s == value.to_s } + else + to_table == options_or_to_table.to_s + end + end + + private + def default_primary_key + "id" + end + end + + class ReferenceDefinition # :nodoc: + def initialize( + name, + polymorphic: false, + index: false, + foreign_key: false, + type: :integer, + **options + ) + @name = name + @polymorphic = polymorphic + @index = index + @foreign_key = foreign_key + @type = type + @options = options + + if polymorphic && foreign_key + raise ArgumentError, "Cannot add a foreign key to a polymorphic relation" + end + end + + def add_to(table) + columns.each do |column_options| + table.column(*column_options) + end + + if index + table.index(column_names, index_options) + end + + if foreign_key + table.foreign_key(foreign_table_name, foreign_key_options) + end + end + + protected + + attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options + + private + + def as_options(value, default = {}) + if value.is_a?(Hash) + value + else + default + end + end + + def polymorphic_options + as_options(polymorphic, options) + end + + def index_options + as_options(index) + end + + def foreign_key_options + as_options(foreign_key) + end + + def columns + result = [["#{name}_id", type, options]] + if polymorphic + result.unshift(["#{name}_type", :string, polymorphic_options]) + end + result + end + + def column_names + columns.map(&:first) + end + + def foreign_table_name + name.to_s.pluralize + end end # Represents the schema of an SQL table in an abstract way. This class @@ -49,11 +166,12 @@ module ActiveRecord # An array of ColumnDefinition objects, representing the column changes # that have been defined. attr_accessor :indexes - attr_reader :name, :temporary, :options, :as + attr_reader :name, :temporary, :options, :as, :foreign_keys def initialize(types, name, temporary, options, as = nil) @columns_hash = {} @indexes = {} + @foreign_keys = {} @native = types @temporary = temporary @options = options @@ -79,8 +197,8 @@ module ActiveRecord # which is one of the following: # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, - # <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, - # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. + # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, + # <tt>:binary</tt>, <tt>:boolean</tt>. # # You may use a type not in this list as long as it is supported by your # database (for example, "polygon" in MySQL), but this will not be database @@ -181,7 +299,7 @@ module ActiveRecord # t.integer :shop_id, :creator_id # t.string :item_number, index: true # t.string :name, :value, default: "Untitled" - # t.timestamps + # t.timestamps null: false # end # # There's a short-hand method for each of the type values declared at the top. And then there's @@ -211,7 +329,7 @@ module ActiveRecord name = name.to_s type = type.to_sym - if primary_key_column_name == name + if @columns_hash[name] && @columns_hash[name].primary_key? raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." end @@ -225,7 +343,7 @@ module ActiveRecord @columns_hash.delete name.to_s end - [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| + [:string, :text, :integer, :bigint, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| define_method column_type do |*args| options = args.extract_options! column_names = args @@ -241,41 +359,56 @@ module ActiveRecord indexes[column_name] = options end + def foreign_key(table_name, options = {}) # :nodoc: + foreign_keys[table_name] = options + end + # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and - # <tt>:updated_at</tt> to the table. + # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps + # + # t.timestamps null: false def timestamps(*args) options = args.extract_options! + + options[:null] = false if options[:null].nil? + column(:created_at, :datetime, options) column(:updated_at, :datetime, options) end - def references(*args) - options = args.extract_options! - polymorphic = options.delete(:polymorphic) - index_options = options.delete(:index) + # Adds a reference. Optionally adds a +type+ column, if the + # +:polymorphic+ option is provided. +references+ and +belongs_to+ + # are acceptable. The reference column will be an +integer+ by default, + # the +:type+ option can be used to specify a different type. A foreign + # key will be created if the +:foreign_key+ option is passed. + # + # t.references(:user) + # t.references(:user, type: "string") + # t.belongs_to(:supplier, polymorphic: true) + # + # See SchemaStatements#add_reference + def references(*args, **options) args.each do |col| - column("#{col}_id", :integer, options) - column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic - index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options + ReferenceDefinition.new(col, **options).add_to(self) end end alias :belongs_to :references def new_column_definition(name, type, options) # :nodoc: - type = aliased_types[type] || type + type = aliased_types(type.to_s, type) column = create_column_definition name, type limit = options.fetch(:limit) do native[type][:limit] if native[type].is_a?(Hash) end column.limit = limit - column.array = options[:array] if column.respond_to?(:array) column.precision = options[:precision] column.scale = options[:scale] column.default = options[:default] column.null = options[:null] column.first = options[:first] column.after = options[:after] + column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] column end @@ -285,32 +418,37 @@ module ActiveRecord ColumnDefinition.new name, type end - def primary_key_column_name - primary_key_column = columns.detect { |c| c.primary_key? } - primary_key_column && primary_key_column.name - end - def native @native end - def aliased_types - HashWithIndifferentAccess.new( - timestamp: :datetime, - ) + def aliased_types(name, fallback) + 'timestamp' == name ? :datetime : fallback end end class AlterTable # :nodoc: attr_reader :adds + attr_reader :foreign_key_adds + attr_reader :foreign_key_drops def initialize(td) @td = td @adds = [] + @foreign_key_adds = [] + @foreign_key_drops = [] end def name; @td.name; end + def add_foreign_key(to_table, options) + @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options) + end + + def drop_foreign_key(name) + @foreign_key_drops << name + end + def add_column(name, type, options) name = name.to_s type = type.to_sym @@ -352,156 +490,183 @@ module ActiveRecord # end # class Table + attr_reader :name + def initialize(table_name, base) - @table_name = table_name + @name = table_name @base = base end # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. # - # ====== Creating a simple column # t.column(:name, :string) + # + # See TableDefinition#column for details of the options you can use. def column(column_name, type, options = {}) - @base.add_column(@table_name, column_name, type, options) + @base.add_column(name, column_name, type, options) end - # Checks to see if a column exists. See SchemaStatements#column_exists? + # Checks to see if a column exists. + # + # See SchemaStatements#column_exists? def column_exists?(column_name, type = nil, options = {}) - @base.column_exists?(@table_name, column_name, type, options) + @base.column_exists?(name, column_name, type, options) end # Adds a new index to the table. +column_name+ can be a single Symbol, or - # an Array of Symbols. See SchemaStatements#add_index + # an Array of Symbols. # - # ====== Creating a simple index # t.index(:name) - # ====== Creating a unique index # t.index([:branch_id, :party_id], unique: true) - # ====== Creating a named index # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') + # + # See SchemaStatements#add_index for details of the options you can use. def index(column_name, options = {}) - @base.add_index(@table_name, column_name, options) + @base.add_index(name, column_name, options) end - # Checks to see if an index exists. See SchemaStatements#index_exists? + # Checks to see if an index exists. + # + # See SchemaStatements#index_exists? def index_exists?(column_name, options = {}) - @base.index_exists?(@table_name, column_name, options) + @base.index_exists?(name, column_name, options) end # Renames the given index on the table. # # t.rename_index(:user_id, :account_id) + # + # See SchemaStatements#rename_index def rename_index(index_name, new_index_name) - @base.rename_index(@table_name, index_name, new_index_name) + @base.rename_index(name, index_name, new_index_name) end - # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps + # Adds timestamps (+created_at+ and +updated_at+) columns to the table. + # + # t.timestamps(null: false) # - # t.timestamps - def timestamps - @base.add_timestamps(@table_name) + # See SchemaStatements#add_timestamps + def timestamps(options = {}) + @base.add_timestamps(name, options) end # Changes the column's definition according to the new options. - # See TableDefinition#column for details of the options you can use. # # t.change(:name, :string, limit: 80) # t.change(:description, :text) + # + # See TableDefinition#column for details of the options you can use. def change(column_name, type, options = {}) - @base.change_column(@table_name, column_name, type, options) + @base.change_column(name, column_name, type, options) end - # Sets a new default value for a column. See SchemaStatements#change_column_default + # Sets a new default value for a column. # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) + # + # See SchemaStatements#change_column_default def change_default(column_name, default) - @base.change_column_default(@table_name, column_name, default) + @base.change_column_default(name, column_name, default) end # Removes the column(s) from the table definition. # # t.remove(:qualification) # t.remove(:qualification, :experience) + # + # See SchemaStatements#remove_columns def remove(*column_names) - @base.remove_columns(@table_name, *column_names) + @base.remove_columns(name, *column_names) end # Removes the given index from the table. # - # ====== Remove the index_table_name_on_column in the table_name table - # t.remove_index :column - # ====== Remove the index named index_table_name_on_branch_id in the table_name table - # t.remove_index column: :branch_id - # ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table - # t.remove_index column: [:branch_id, :party_id] - # ====== Remove the index named by_branch_party in the table_name table - # t.remove_index name: :by_branch_party + # t.remove_index(:branch_id) + # t.remove_index(column: [:branch_id, :party_id]) + # t.remove_index(name: :by_branch_party) + # + # See SchemaStatements#remove_index def remove_index(options = {}) - @base.remove_index(@table_name, options) + @base.remove_index(name, options) end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. # # t.remove_timestamps - def remove_timestamps - @base.remove_timestamps(@table_name) + # + # See SchemaStatements#remove_timestamps + def remove_timestamps(options = {}) + @base.remove_timestamps(name, options) end # Renames a column. # # t.rename(:description, :name) + # + # See SchemaStatements#rename_column def rename(column_name, new_column_name) - @base.rename_column(@table_name, column_name, new_column_name) + @base.rename_column(name, column_name, new_column_name) end - # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. + # Adds a reference. Optionally adds a +type+ column, if + # <tt>:polymorphic</tt> option is provided. # # t.references(:user) + # t.references(:user, type: "string") # t.belongs_to(:supplier, polymorphic: true) + # t.belongs_to(:supplier, foreign_key: true) # + # See SchemaStatements#add_reference def references(*args) options = args.extract_options! args.each do |ref_name| - @base.add_reference(@table_name, ref_name, options) + @base.add_reference(name, ref_name, options) end end alias :belongs_to :references # Removes a reference. Optionally removes a +type+ column. - # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. # # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) # + # See SchemaStatements#remove_reference def remove_references(*args) options = args.extract_options! args.each do |ref_name| - @base.remove_reference(@table_name, ref_name, options) + @base.remove_reference(name, ref_name, options) end end alias :remove_belongs_to :remove_references - # Adds a column or columns of a specified type + # Adds a column or columns of a specified type. # # t.string(:goat) # t.string(:goat, :sheep) + # + # See SchemaStatements#add_column [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| define_method column_type do |*args| options = args.extract_options! - args.each do |name| - @base.add_column(@table_name, name, column_type, options) + args.each do |column_name| + @base.add_column(name, column_name, column_type, options) end end end + def foreign_key(*args) # :nodoc: + @base.add_foreign_key(name, *args) + end + + def foreign_key_exists?(*args) # :nodoc: + @base.foreign_key_exists?(name, *args) + end + private def native @base.native_database_types end end - end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 5a0efe49c7..af7ef7cbaa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -6,24 +6,35 @@ module ActiveRecord # We can then redefine how certain data types may be handled in the schema dumper on the # Adapter level by over-writing this code inside the database specific adapters module ColumnDumper - def column_spec(column, types) - spec = prepare_column_options(column, types) - (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")} + def column_spec(column) + spec = prepare_column_options(column) + (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k}: ")} spec end + def column_spec_for_primary_key(column) + return if column.type == :integer + spec = { id: column.type.inspect } + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) }) + end + # This can be overridden on a Adapter level basis to support other # extended datatypes (Example: Adding an array option in the # PostgreSQLAdapter) - def prepare_column_options(column, types) + def prepare_column_options(column) spec = {} spec[:name] = column.name.inspect spec[:type] = column.type.to_s - spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] + spec[:null] = 'false' unless column.null + + limit = column.limit || native_database_types[column.type][:limit] + spec[:limit] = limit.inspect if limit spec[:precision] = column.precision.inspect if column.precision spec[:scale] = column.scale.inspect if column.scale - spec[:null] = 'false' unless column.null - spec[:default] = column.type_cast_for_schema(column.default) if column.has_default? + + default = schema_default(column) if column.has_default? + spec[:default] = default unless default.nil? + spec end @@ -31,6 +42,16 @@ module ActiveRecord def migration_keys [:name, :limit, :precision, :scale, :default, :null] end + + private + + def schema_default(column) + type = lookup_cast_type_from_column(column) + default = type.deserialize(column.default) + unless default.nil? + type.type_cast_for_schema(default) + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index ffa6af6d99..503b09d1fc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,4 +1,6 @@ require 'active_record/migration/join_table' +require 'active_support/core_ext/string/access' +require 'digest' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -43,13 +45,14 @@ module ActiveRecord # index_exists?(:suppliers, :company_id, name: "idx_company_id") # def index_exists?(table_name, column_name, options = {}) - column_names = Array(column_name) - index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names) - if options[:unique] - indexes(table_name).any?{ |i| i.unique && i.name == index_name } - else - indexes(table_name).any?{ |i| i.name == index_name } - end + column_names = Array(column_name).map(&:to_s) + index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names) + checks = [] + checks << lambda { |i| i.name == index_name } + checks << lambda { |i| i.columns == column_names } + checks << lambda { |i| i.unique } if options[:unique] + + indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end # Returns an array of Column objects for the table specified by +table_name+. @@ -131,6 +134,7 @@ module ActiveRecord # Make a temporary table. # [<tt>:force</tt>] # Set to true to drop the table before creating it. + # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. # [<tt>:as</tt>] # SQL to use to generate the table. When this option is used, the block is @@ -202,7 +206,17 @@ module ActiveRecord end result = execute schema_creation.accept td - td.indexes.each_pair { |c, o| add_index(table_name, c, o) } unless supports_indexes_in_create? + + unless supports_indexes_in_create? + td.indexes.each_pair do |column_name, index_options| + add_index(table_name, column_name, index_options) + end + end + + td.foreign_keys.each_pair do |other_table_name, foreign_key_options| + add_foreign_key(table_name, other_table_name, foreign_key_options) + end + result end @@ -360,11 +374,15 @@ module ActiveRecord # Drops a table from the database. # - # Although this command ignores +options+ and the block if one is given, it can be helpful - # to provide these in a migration's +change+ method so it can be reverted. + # [<tt>:force</tt>] + # Set to +:cascade+ to drop dependent objects as well. + # Defaults to false. + # + # Although this command ignores most +options+ and the block if one is given, + # it can be helpful to provide these in a migration's +change+ method so it can be reverted. # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) - execute "DROP TABLE #{quote_table_name(table_name)}" + execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}" end # Adds a new column to the named table. @@ -570,6 +588,8 @@ module ActiveRecord # rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name' # def rename_index(table_name, old_name, new_name) + validate_index_length!(table_name, new_name) + # this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance) old_index_def = indexes(table_name).detect { |i| i.name == old_name } return unless old_index_def @@ -602,26 +622,32 @@ module ActiveRecord end # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. + # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify + # a different type. # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. # - # ====== Create a user_id column + # ====== Create a user_id integer column # # add_reference(:products, :user) # + # ====== Create a user_id string column + # + # add_reference(:products, :user, type: :string) + # # ====== Create a supplier_id and supplier_type columns # # add_belongs_to(:products, :supplier, polymorphic: true) # - # ====== Create a supplier_id, supplier_type columns and appropriate index + # ====== Create supplier_id, supplier_type columns and appropriate index # # add_reference(:products, :supplier, polymorphic: true, index: true) # - def add_reference(table_name, ref_name, options = {}) - polymorphic = options.delete(:polymorphic) - index_options = options.delete(:index) - add_column(table_name, "#{ref_name}_id", :integer, options) - add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic - add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options + # ====== Create a supplier_id column and appropriate foreign key + # + # add_reference(:products, :supplier, foreign_key: true) + # + def add_reference(table_name, *args) + ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference @@ -636,12 +662,138 @@ module ActiveRecord # # remove_reference(:products, :supplier, polymorphic: true) # + # ====== Remove the reference with a foreign key + # + # remove_reference(:products, :user, index: true, foreign_key: true) + # def remove_reference(table_name, ref_name, options = {}) + remove_foreign_key table_name, ref_name.to_s.pluralize if options[:foreign_key] + remove_column(table_name, "#{ref_name}_id") remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] end alias :remove_belongs_to :remove_reference + # Returns an array of foreign keys for the given table. + # The foreign keys are represented as +ForeignKeyDefinition+ objects. + def foreign_keys(table_name) + raise NotImplementedError, "foreign_keys is not implemented" + end + + # Adds a new foreign key. +from_table+ is the table with the key column, + # +to_table+ contains the referenced primary key. + # + # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>. + # +identifier+ is a 10 character long random string. A custom name can be specified with + # the <tt>:name</tt> option. + # + # ====== Creating a simple foreign key + # + # add_foreign_key :articles, :authors + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") + # + # ====== Creating a foreign key on a specific column + # + # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id" + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id") + # + # ====== Creating a cascading foreign key + # + # add_foreign_key :articles, :authors, on_delete: :cascade + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE + # + # The +options+ hash can include the following keys: + # [<tt>:column</tt>] + # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt> + # [<tt>:primary_key</tt>] + # The primary key column name on +to_table+. Defaults to +id+. + # [<tt>:name</tt>] + # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>. + # [<tt>:on_delete</tt>] + # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # [<tt>:on_update</tt>] + # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + def add_foreign_key(from_table, to_table, options = {}) + return unless supports_foreign_keys? + + options[:column] ||= foreign_key_column_for(to_table) + + options = { + column: options[:column], + primary_key: options[:primary_key], + name: foreign_key_name(from_table, options), + on_delete: options[:on_delete], + on_update: options[:on_update] + } + at = create_alter_table from_table + at.add_foreign_key to_table, options + + execute schema_creation.accept(at) + end + + # Removes the given foreign key from the table. + # + # Removes the foreign key on +accounts.branch_id+. + # + # remove_foreign_key :accounts, :branches + # + # Removes the foreign key on +accounts.owner_id+. + # + # remove_foreign_key :accounts, column: :owner_id + # + # Removes the foreign key named +special_fk_name+ on the +accounts+ table. + # + # remove_foreign_key :accounts, name: :special_fk_name + # + def remove_foreign_key(from_table, options_or_to_table = {}) + return unless supports_foreign_keys? + + fk_name_to_delete = foreign_key_for!(from_table, options_or_to_table).name + + at = create_alter_table from_table + at.drop_foreign_key fk_name_to_delete + + execute schema_creation.accept(at) + end + + # Checks to see if a foreign key exists on a table for a given foreign key definition. + # + # # Check a foreign key exists + # foreign_key_exists?(:accounts, :branches) + # + # # Check a foreign key on specified column exists + # foreign_key_exists?(:accounts, column: :owner_id) + # + # # Check a foreign key with a custom name exists + # foreign_key_exists?(:accounts, name: "special_fk_name") + # + def foreign_key_exists?(from_table, options_or_to_table = {}) + foreign_key_for(from_table, options_or_to_table).present? + end + + def foreign_key_for(from_table, options_or_to_table = {}) # :nodoc: + return unless supports_foreign_keys? + foreign_keys(from_table).detect {|fk| fk.defined_for? options_or_to_table } + end + + def foreign_key_for!(from_table, options_or_to_table = {}) # :nodoc: + foreign_key_for(from_table, options_or_to_table) or \ + raise ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}" + end + + def foreign_key_column_for(table_name) # :nodoc: + "#{table_name.to_s.singularize}_id" + end + def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name @@ -661,7 +813,7 @@ module ActiveRecord version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) - migrated = select_values("SELECT version FROM #{sm_table}").map { |v| v.to_i } + migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) paths = migrations_paths.map {|p| "#{p}/[0-9]*_*.rb" } versions = Dir[*paths].map do |filename| filename.split('/').last.split('_').first.to_i @@ -718,20 +870,23 @@ module ActiveRecord columns end - # Adds timestamps (+created_at+ and +updated_at+) columns to the named table. + # Adds timestamps (+created_at+ and +updated_at+) columns to +table_name+. + # Additional options (like <tt>null: false</tt>) are forwarded to #add_column. # - # add_timestamps(:suppliers) + # add_timestamps(:suppliers, null: false) # - def add_timestamps(table_name) - add_column table_name, :created_at, :datetime - add_column table_name, :updated_at, :datetime + def add_timestamps(table_name, options = {}) + options[:null] = false if options[:null].nil? + + add_column table_name, :created_at, :datetime, options + add_column table_name, :updated_at, :datetime, options end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table definition. # # remove_timestamps(:suppliers) # - def remove_timestamps(table_name) + def remove_timestamps(table_name, options = {}) remove_column table_name, :updated_at remove_column table_name, :created_at end @@ -788,7 +943,7 @@ module ActiveRecord return option_strings end - # Overridden by the mysql adapter for supporting index lengths + # Overridden by the MySQL adapter for supporting index lengths def quoted_columns_for_index(column_names, options = {}) option_strings = Hash[column_names.map {|name| [name, '']}] @@ -845,12 +1000,26 @@ module ActiveRecord end private - def create_table_definition(name, temporary, options, as = nil) + def create_table_definition(name, temporary = false, options = nil, as = nil) TableDefinition.new native_database_types, name, temporary, options, as end def create_alter_table(name) - AlterTable.new create_table_definition(name, false, {}) + AlterTable.new create_table_definition(name) + end + + def foreign_key_name(table_name, options) # :nodoc: + identifier = "#{table_name}_#{options.fetch(:column)}_fk" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + options.fetch(:name) do + "fk_rails_#{hashed_identifier}" + end + end + + def validate_index_length!(table_name, new_name) + if new_name.length > allowed_index_name_length + raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index bc4884b538..11440e30d4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -1,20 +1,7 @@ module ActiveRecord module ConnectionAdapters - class Transaction #:nodoc: - attr_reader :connection - - def initialize(connection) - @connection = connection - @state = TransactionState.new - end - - def state - @state - end - end - class TransactionState - attr_accessor :parent + attr_reader :parent VALID_STATES = Set.new([:committed, :rolledback, nil]) @@ -35,6 +22,10 @@ module ActiveRecord @state == :rolledback end + def completed? + committed? || rolledback? + end + def set_state(state) if !VALID_STATES.include?(state) raise ArgumentError, "Invalid transaction state: #{state}" @@ -43,127 +34,93 @@ module ActiveRecord end end - class ClosedTransaction < Transaction #:nodoc: - def number - 0 - end - - def begin(options = {}) - RealTransaction.new(connection, self, options) - end - - def closed? - true - end - - def open? - false - end - - def joinable? - false - end - - # This is a noop when there are no open transactions - def add_record(record) - end + class NullTransaction #:nodoc: + def initialize; end + def closed?; true; end + def open?; false; end + def joinable?; false; end + def add_record(record); end end - class OpenTransaction < Transaction #:nodoc: - attr_reader :parent, :records - attr_writer :joinable + class Transaction #:nodoc: - def initialize(connection, parent, options = {}) - super connection + attr_reader :connection, :state, :records, :savepoint_name + attr_writer :joinable - @parent = parent - @records = [] - @finishing = false - @joinable = options.fetch(:joinable, true) + def initialize(connection, options) + @connection = connection + @state = TransactionState.new + @records = [] + @joinable = options.fetch(:joinable, true) end - # This state is necessary so that we correctly handle stuff that might - # happen in a commit/rollback. But it's kinda distasteful. Maybe we can - # find a better way to structure it in the future. - def finishing? - @finishing + def add_record(record) + records << record end - def joinable? - @joinable && !finishing? + def rollback + @state.set_state(:rolledback) end - def number - if finishing? - parent.number - else - parent.number + 1 + def rollback_records + ite = records.uniq + while record = ite.shift + record.rolledback!(force_restore_state: full_rollback?) end - end - - def begin(options = {}) - if finishing? - parent.begin - else - SavepointTransaction.new(connection, self, options) + ensure + ite.each do |i| + i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false) end end - def rollback - @finishing = true - perform_rollback - parent - end - def commit - @finishing = true - perform_commit - parent + @state.set_state(:committed) end - def add_record(record) - if record.has_transactional_callbacks? - records << record - else - record.set_transaction_state(@state) + def commit_records + ite = records.uniq + while record = ite.shift + record.committed! end - end - - def rollback_records - @state.set_state(:rolledback) - records.uniq.each do |record| - begin - record.rolledback!(parent.closed?) - rescue => e - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end + ensure + ite.each do |i| + i.committed!(should_run_callbacks: false) end end - def commit_records - @state.set_state(:committed) - records.uniq.each do |record| - begin - record.committed! - rescue => e - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end + def full_rollback?; true; end + def joinable?; @joinable; end + def closed?; false; end + def open?; !closed?; end + end + + class SavepointTransaction < Transaction + + def initialize(connection, savepoint_name, options) + super(connection, options) + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" end + connection.create_savepoint(@savepoint_name = savepoint_name) end - def closed? - false + def rollback + connection.rollback_to_savepoint(savepoint_name) + super end - def open? - true + def commit + connection.release_savepoint(savepoint_name) + super end + + def full_rollback?; false; end end - class RealTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) - super + class RealTransaction < Transaction + def initialize(connection, options) + super if options[:isolation] connection.begin_isolated_db_transaction(options[:isolation]) else @@ -171,37 +128,85 @@ module ActiveRecord end end - def perform_rollback + def rollback connection.rollback_db_transaction - rollback_records + super end - def perform_commit + def commit connection.commit_db_transaction - commit_records + super end end - class SavepointTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) - if options[:isolation] - raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" + class TransactionManager #:nodoc: + def initialize(connection) + @stack = [] + @connection = connection + end + + def begin_transaction(options = {}) + transaction = + if @stack.empty? + RealTransaction.new(@connection, options) + else + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options) + end + + @stack.push(transaction) + transaction + end + + def commit_transaction + inner_transaction = @stack.pop + inner_transaction.commit + + if current_transaction.joinable? + inner_transaction.records.each do |r| + r.add_to_transaction + end + else + inner_transaction.commit_records end + end - super - connection.create_savepoint + def rollback_transaction(transaction = nil) + transaction ||= @stack.pop + transaction.rollback + transaction.rollback_records + end + + def within_new_transaction(options = {}) + transaction = begin_transaction options + yield + rescue Exception => error + rollback_transaction if transaction + raise + ensure + unless error + if Thread.current.status == 'aborting' + rollback_transaction if transaction + else + begin + commit_transaction + rescue Exception + rollback_transaction(transaction) unless transaction.state.completed? + raise + end + end + end end - def perform_rollback - connection.rollback_to_savepoint - rollback_records + def open_transactions + @stack.size end - def perform_commit - @state.set_state(:committed) - @state.parent = parent.state - connection.release_savepoint + def current_transaction + @stack.last || NULL_TRANSACTION end + + private + NULL_TRANSACTION = NullTransaction.new end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index cc494a7f40..ee11c0efa4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ require 'bigdecimal/util' require 'active_record/type' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' +require 'active_record/connection_adapters/sql_type_metadata' require 'active_record/connection_adapters/abstract/schema_dumper' require 'active_record/connection_adapters/abstract/schema_creation' require 'monitor' @@ -14,16 +15,14 @@ module ActiveRecord module ConnectionAdapters # :nodoc: extend ActiveSupport::Autoload - autoload_at 'active_record/connection_adapters/column' do - autoload :Column - autoload :NullColumn - end + autoload :Column autoload :ConnectionSpecification autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do autoload :IndexDefinition autoload :ColumnDefinition autoload :ChangeColumnDefinition + autoload :ForeignKeyDefinition autoload :TableDefinition autoload :Table autoload :AlterTable @@ -45,7 +44,8 @@ module ActiveRecord end autoload_at 'active_record/connection_adapters/abstract/transaction' do - autoload :ClosedTransaction + autoload :TransactionManager + autoload :NullTransaction autoload :RealTransaction autoload :SavepointTransaction autoload :TransactionState @@ -65,6 +65,7 @@ module ActiveRecord # Most of the methods in the adapter are useful during migrations. Most # notably, the instance methods provided by SchemaStatement are very useful. class AbstractAdapter + ADAPTER_NAME = 'Abstract'.freeze include Quoting, DatabaseStatements, SchemaStatements include DatabaseLimits include QueryCache @@ -113,7 +114,8 @@ module ActiveRecord class BindCollector < Arel::Collectors::Bind def compile(bvs, conn) - super(bvs.map { |bv| conn.quote(*bv.reverse) }) + casted_binds = conn.prepare_binds_for_database(bvs) + super(casted_binds.map { |value| conn.quote(value) }) end end @@ -166,7 +168,7 @@ module ActiveRecord # Returns the human-readable name of the adapter. Use mixed case - one # can always use downcase if needed. def adapter_name - 'Abstract' + self.class::ADAPTER_NAME end # Does this adapter support migrations? @@ -233,6 +235,16 @@ module ActiveRecord false end + # Does this adapter support creating foreign key constraints? + def supports_foreign_keys? + false + end + + # Does this adapter support views? + def supports_views? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -251,12 +263,10 @@ module ActiveRecord {} end - # QUOTING ================================================== - - # Returns a bind substitution value given a bind +index+ and +column+ + # Returns a bind substitution value given a bind +column+ # NOTE: The column param is currently being used by the sqlserver-adapter - def substitute_at(column, index) - Arel::Nodes::BindParam.new '?' + def substitute_at(column, _unused = 0) + Arel::Nodes::BindParam.new end # REFERENTIAL INTEGRITY ==================================== @@ -331,9 +341,6 @@ module ActiveRecord def create_savepoint(name = nil) end - def rollback_to_savepoint(name = nil) - end - def release_savepoint(name = nil) end @@ -348,11 +355,20 @@ module ActiveRecord end def case_insensitive_comparison(table, attribute, column, value) - table[attribute].lower.eq(table.lower(value)) + if can_perform_case_insensitive_comparison_for?(column) + table[attribute].lower.eq(table.lower(value)) + else + case_sensitive_comparison(table, attribute, column, value) + end + end + + def can_perform_case_insensitive_comparison_for?(column) + true end + private :can_perform_case_insensitive_comparison_for? def current_savepoint_name - "active_record_#{open_transactions}" + current_transaction.savepoint_name end # Check the connection back in to the connection pool @@ -366,26 +382,30 @@ module ActiveRecord end end - def new_column(name, default, cast_type, sql_type = nil, null = true) - Column.new(name, default, cast_type, sql_type, null) + def new_column(name, default, sql_type_metadata = nil, null = true) + Column.new(name, default, sql_type_metadata, null) end - protected - def lookup_cast_type(sql_type) # :nodoc: type_map.lookup(sql_type) end + def column_name_for_operation(operation, node) # :nodoc: + visitor.accept(node, collector).value + end + + protected + def initialize_type_map(m) # :nodoc: - register_class_with_limit m, %r(boolean)i, Type::Boolean - register_class_with_limit m, %r(char)i, Type::String - register_class_with_limit m, %r(binary)i, Type::Binary - register_class_with_limit m, %r(text)i, Type::Text - register_class_with_limit m, %r(date)i, Type::Date - register_class_with_limit m, %r(time)i, Type::Time - register_class_with_limit m, %r(datetime)i, Type::DateTime - register_class_with_limit m, %r(float)i, Type::Float - register_class_with_limit m, %r(int)i, Type::Integer + register_class_with_limit m, %r(boolean)i, Type::Boolean + register_class_with_limit m, %r(char)i, Type::String + register_class_with_limit m, %r(binary)i, Type::Binary + register_class_with_limit m, %r(text)i, Type::Text + register_class_with_precision m, %r(date)i, Type::Date + register_class_with_precision m, %r(time)i, Type::Time + register_class_with_precision m, %r(datetime)i, Type::DateTime + register_class_with_limit m, %r(float)i, Type::Float + register_class_with_limit m, %r(int)i, Type::Integer m.alias_type %r(blob)i, 'binary' m.alias_type %r(clob)i, 'text' @@ -419,6 +439,13 @@ module ActiveRecord end end + def register_class_with_precision(mapping, key, klass) # :nodoc: + mapping.register_type(key) do |*args| + precision = extract_precision(args.last) + klass.new(precision: precision) + end + end + def extract_scale(sql_type) # :nodoc: case sql_type when /\((\d+)\)/ then 0 @@ -431,11 +458,21 @@ module ActiveRecord end def extract_limit(sql_type) # :nodoc: - $1.to_i if sql_type =~ /\((.*)\)/ + case sql_type + when /^bigint/i + 8 + when /\((.*)\)/ + $1.to_i + end end def translate_exception_class(e, sql) - message = "#{e.class.name}: #{e.message}: #{sql}" + begin + message = "#{e.class.name}: #{e.message}: #{sql}" + rescue Encoding::CompatibilityError + message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}" + end + @logger.error message if @logger exception = translate_exception(e, message) exception.set_backtrace e.backtrace diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 200b773172..0a1c66be1e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,10 +1,18 @@ require 'arel/visitors/bind_visitor' +require 'active_support/core_ext/string/strip' module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter include Savepoints + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + def primary_key(name, type = :primary_key, options = {}) + options[:auto_increment] ||= type == :bigint + super + end + end + class SchemaCreation < AbstractAdapter::SchemaCreation def visit_AddColumn(o) add_column_position!(super, column_options(o)) @@ -12,6 +20,10 @@ module ActiveRecord private + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + def visit_TableDefinition(o) name = o.name create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} " @@ -26,12 +38,8 @@ module ActiveRecord end def visit_ChangeColumnDefinition(o) - column = o.column - options = o.options - sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale]) - change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}" - add_column_options!(change_column_sql, options.merge(column: column)) - add_column_position!(change_column_sql, options) + change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" + add_column_position!(change_column_sql, column_options(o.column)) end def add_column_position!(sql, options) @@ -53,29 +61,43 @@ module ActiveRecord SchemaCreation.new self end + def column_spec_for_primary_key(column) + spec = {} + if column.auto_increment? + return unless column.limit == 8 + spec[:id] = ':bigint' + else + spec[:id] = column.type.inspect + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + end + spec + end + + def prepare_column_options(column) + spec = super + spec.delete(:precision) if column.type == :datetime && column.precision == 0 + spec + end + class Column < ConnectionAdapters::Column # :nodoc: - attr_reader :collation, :strict, :extra + delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true - def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "") - @strict = strict - @collation = collation - @extra = extra - super(name, default, cast_type, sql_type, null) + def initialize(*) + super assert_valid_default(default) + extract_default end - def default - @default ||= if blob_or_text_column? - null || strict ? nil : '' - elsif missing_default_forged_as_empty_string?(@original_default) - nil - else - super + def extract_default + if blob_or_text_column? + @default = null || strict ? nil : '' + elsif missing_default_forged_as_empty_string?(default) + @default = nil end end def has_default? - return false if blob_or_text_column? #mysql forbids defaults on blob and text columns + return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns super end @@ -87,6 +109,10 @@ module ActiveRecord collation && !collation.match(/_ci$/) end + def auto_increment? + extra == 'auto_increment' + end + private # MySQL misreports NOT NULL column default when none is given. @@ -107,6 +133,34 @@ module ActiveRecord end end + class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + attr_reader :collation, :extra, :strict + + def initialize(type_metadata, collation: "", extra: "", strict: false) + super(type_metadata) + @type_metadata = type_metadata + @collation = collation + @extra = extra + @strict = strict + end + + def ==(other) + other.is_a?(MysqlTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, @type_metadata, collation, extra, strict] + end + end + ## # :singleton-method: # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> @@ -158,10 +212,6 @@ module ActiveRecord end end - def adapter_name #:nodoc: - self.class::ADAPTER_NAME - end - # Returns true, since this connection adapter supports migrations. def supports_migrations? true @@ -193,6 +243,14 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + + def supports_views? + version[0] >= 5 + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -209,11 +267,11 @@ module ActiveRecord raise NotImplementedError end - def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc: - Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) + def new_column(field, default, sql_type_metadata = nil, null = true) # :nodoc: + Column.new(field, default, sql_type_metadata, null) end - # Must return the Mysql error number from the exception, if the exception has an + # Must return the MySQL error number from the exception, if the exception has an # error number. def error_number(exception) # :nodoc: raise NotImplementedError @@ -266,7 +324,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def clear_cache! super @@ -303,7 +363,7 @@ module ActiveRecord execute "COMMIT" end - def rollback_db_transaction #:nodoc: + def exec_rollback_db_transaction #:nodoc: execute "ROLLBACK" end @@ -377,12 +437,16 @@ module ActiveRecord sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| - result.collect { |field| field.first } + result.collect(&:first) end end + def truncate(table_name, name = nil) + execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name + end + def table_exists?(name) - return false unless name + return false unless name.present? return true if tables(nil, nil, name).any? name = name.to_s @@ -427,8 +491,8 @@ module ActiveRecord each_hash(result).map do |field| field_name = set_field_encoding(field[:Field]) sql_type = field[:Type] - cast_type = lookup_cast_type(sql_type) - new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra]) + type_metadata = fetch_type_metadata(sql_type, field[:Collation], field[:Extra]) + new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES") end end end @@ -462,18 +526,20 @@ module ActiveRecord end def drop_table(table_name, options = {}) - execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}" + execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end def rename_index(table_name, old_name, new_name) - if (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + if supports_rename_index? + validate_index_length!(table_name, new_name) + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}" else super end end - def change_column_default(table_name, column_name, default) + def change_column_default(table_name, column_name, default) #:nodoc: column = column_for(table_name, column_name) change_column table_name, column_name, column.sql_type, :default => default end @@ -502,6 +568,34 @@ module ActiveRecord execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT fk.referenced_table_name as 'to_table' + ,fk.referenced_column_name as 'primary_key' + ,fk.column_name as 'column' + ,fk.constraint_name as 'name' + FROM information_schema.key_column_usage fk + WHERE fk.referenced_column_name is not null + AND fk.table_schema = '#{@config[:database]}' + AND fk.table_name = '#{table_name}' + SQL + + create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE") + options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE") + + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s @@ -529,19 +623,18 @@ module ActiveRecord when 0x1000000..0xffffffff; 'longtext' else raise(ActiveRecordError, "No text type has character length #{limit}") end + when 'datetime' + return super unless precision + + case precision + when 0..6; "datetime(#{precision})" + else raise(ActiveRecordError, "No datetime type has precision of #{precision}. The allowed range of precision is from 0 to 6.") + end else super end end - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - end - # SHOW VARIABLES LIKE 'name' def show_variable(name) variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') @@ -588,10 +681,6 @@ module ActiveRecord end end - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) - where_sql - end - def strict_mode? self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end @@ -604,30 +693,58 @@ module ActiveRecord def initialize_type_map(m) # :nodoc: super - m.register_type(%r(enum)i) do |sql_type| - limit = sql_type[/^enum\((.+)\)/i, 1] - .split(',').map{|enum| enum.strip.length - 2}.max - Type::String.new(limit: limit) - end - - m.register_type %r(tinytext)i, Type::Text.new(limit: 255) - m.register_type %r(tinyblob)i, Type::Binary.new(limit: 255) - m.register_type %r(mediumtext)i, Type::Text.new(limit: 16777215) - m.register_type %r(mediumblob)i, Type::Binary.new(limit: 16777215) - m.register_type %r(longtext)i, Type::Text.new(limit: 2147483647) - m.register_type %r(longblob)i, Type::Binary.new(limit: 2147483647) - m.register_type %r(^bigint)i, Type::Integer.new(limit: 8) - m.register_type %r(^int)i, Type::Integer.new(limit: 4) - m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3) - m.register_type %r(^smallint)i, Type::Integer.new(limit: 2) - m.register_type %r(^tinyint)i, Type::Integer.new(limit: 1) + + register_class_with_limit m, %r(char)i, MysqlString + + m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1) + m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1) + m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1) + m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1) + m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1) + m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1) + m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1) + m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) m.register_type %r(^float)i, Type::Float.new(limit: 24) m.register_type %r(^double)i, Type::Float.new(limit: 53) + register_integer_type m, %r(^bigint)i, limit: 8 + register_integer_type m, %r(^int)i, limit: 4 + register_integer_type m, %r(^mediumint)i, limit: 3 + register_integer_type m, %r(^smallint)i, limit: 2 + register_integer_type m, %r(^tinyint)i, limit: 1 + m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans m.alias_type %r(set)i, 'varchar' m.alias_type %r(year)i, 'integer' m.alias_type %r(bit)i, 'binary' + + m.register_type(%r(enum)i) do |sql_type| + limit = sql_type[/^enum\((.+)\)/i, 1] + .split(',').map{|enum| enum.strip.length - 2}.max + MysqlString.new(limit: limit) + end + end + + def register_integer_type(mapping, key, options) # :nodoc: + mapping.register_type(key) do |sql_type| + if /unsigned/i =~ sql_type + Type::UnsignedInteger.new(options) + else + Type::Integer.new(options) + end + end + end + + def extract_precision(sql_type) + if /datetime/ === sql_type + super || 0 + else + super + end + end + + def fetch_type_metadata(sql_type, collation = "", extra = "") + MysqlTypeMetadata.new(super(sql_type), collation: collation, extra: extra, strict: strict_mode?) end # MySQL is too stupid to create a temporary table for use subquery, so we have @@ -678,7 +795,7 @@ module ActiveRecord end def add_column_sql(table_name, column_name, type, options = {}) - td = create_table_definition table_name, options[:temporary], options[:options] + td = create_table_definition(table_name) cd = td.new_column_definition(column_name, type, options) schema_creation.visit_AddColumn cd end @@ -694,21 +811,23 @@ module ActiveRecord options[:null] = column.null end - options[:name] = column.name - schema_creation.accept ChangeColumnDefinition.new column, type, options + td = create_table_definition(table_name) + cd = td.new_column_definition(column.name, type, options) + schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end def rename_column_sql(table_name, column_name, new_column_name) column = column_for(table_name, column_name) options = { - name: new_column_name, default: column.default, null: column.null, - auto_increment: column.extra == "auto_increment" + auto_increment: column.auto_increment? } current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] - schema_creation.accept ChangeColumnDefinition.new column, current_type, options + td = create_table_definition(table_name) + cd = td.new_column_definition(new_column_name, current_type, options) + schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end def remove_column_sql(table_name, column_name, type = nil, options = {}) @@ -729,18 +848,26 @@ module ActiveRecord "DROP INDEX #{index_name}" end - def add_timestamps_sql(table_name) - [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)] + def add_timestamps_sql(table_name, options = {}) + [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)] end - def remove_timestamps_sql(table_name) + def remove_timestamps_sql(table_name, options = {}) [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] end private - def supports_views? - version[0] >= 5 + def version + @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) + end + + def mariadb? + full_version =~ /mariadb/i + end + + def supports_rename_index? + mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 end def configure_connection @@ -758,21 +885,25 @@ module ActiveRecord # Make MySQL reject illegal values rather than truncating or blanking them, see # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. - if strict_mode? && !variables.has_key?('sql_mode') - variables['sql_mode'] = 'STRICT_ALL_TABLES' + unless variables.has_key?('sql_mode') + variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see # http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430 # (trailing comma because variable_assignments will always have content) - encoding = "NAMES #{@config[:encoding]}, " if @config[:encoding] + if @config[:encoding] + encoding = "NAMES #{@config[:encoding]}" + encoding << " COLLATE #{@config[:collation]}" if @config[:collation] + encoding << ", " + end # Gather up all of the SET variables... variable_assignments = variables.map do |k, v| if v == ':default' || v == :default - "@@SESSION.#{k.to_s} = DEFAULT" # Sets the value to the global or compile default + "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default elsif !v.nil? - "@@SESSION.#{k.to_s} = #{quote(v)}" + "@@SESSION.#{k} = #{quote(v)}" end # or else nil; compact to clear nils out end.compact.join(', ') @@ -780,6 +911,42 @@ module ActiveRecord # ...and send them all in one query @connection.query "SET #{encoding} #{variable_assignments}" end + + def extract_foreign_key_action(structure, name, action) # :nodoc: + if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ + case $1 + when 'CASCADE'; :cascade + when 'SET NULL'; :nullify + end + end + end + + def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: + TableDefinition.new(native_database_types, name, temporary, options, as) + end + + class MysqlString < Type::String # :nodoc: + def serialize(value) + case value + when true then "1" + when false then "0" + else super + end + end + + private + + def cast_value(value) + case value + when true then "1" + when false then "0" + else super + end + end + end + + ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) + ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 72c6990ba5..fa5ed07b8a 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -5,7 +5,6 @@ module ActiveRecord module ConnectionAdapters # An abstract definition of a column in a table. class Column - TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON'].to_set FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set module Format @@ -13,30 +12,22 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :cast_type, :null, :sql_type, :default_function + attr_reader :name, :null, :sql_type_metadata, :default, :default_function - delegate :type, :precision, :scale, :limit, :klass, :accessor, - :text?, :number?, :binary?, :serialized?, :changed?, - :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, - :type_cast_for_schema, - to: :cast_type + delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true # Instantiates a new column in the table. # # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. - # +cast_type+ is the object used for type casting and type information. - # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in - # <tt>company_name varchar(60)</tt>. - # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. + # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, cast_type, sql_type = nil, null = true) - @name = name - @cast_type = cast_type - @sql_type = sql_type - @null = null - @original_default = default - @default_function = nil + def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil) + @name = name + @sql_type_metadata = sql_type_metadata + @null = null + @default = default + @default_function = default_function end def has_default? @@ -51,21 +42,26 @@ module ActiveRecord Base.human_attribute_name(@name) end - def default - @default ||= type_cast_from_database(@original_default) + def ==(other) + other.is_a?(Column) && + attributes_for_hash == other.attributes_for_hash end + alias :eql? :== - def with_type(type) - dup.tap do |clone| - clone.instance_variable_set('@default', nil) - clone.instance_variable_set('@cast_type', type) - end + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, name, default, sql_type_metadata, null, default_function] end end class NullColumn < Column def initialize(name) - super name, nil, Type::Value.new + super(name, nil) end end end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index b79d1a4458..08d46fca96 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -32,8 +32,8 @@ module ActiveRecord # } def initialize(url) raise "Database URL cannot be empty" if url.blank? - @uri = URI.parse(url) - @adapter = @uri.scheme.gsub('-', '_') + @uri = uri_parser.parse(url) + @adapter = @uri.scheme.tr('-', '_') @adapter = "postgresql" if @adapter == "postgres" if @uri.opaque @@ -85,7 +85,7 @@ module ActiveRecord "password" => uri.password, "port" => uri.port, "database" => database_from_path, - "host" => uri.host }) + "host" => uri.hostname }) end end @@ -160,7 +160,7 @@ module ActiveRecord # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } } # spec = Resolver.new(config).spec(:production) # spec.adapter_method - # # => "sqlite3" + # # => "sqlite3_connection" # spec.config # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } # @@ -209,27 +209,13 @@ module ActiveRecord when Symbol resolve_symbol_connection spec when String - resolve_string_connection spec + resolve_url_connection spec when Hash resolve_hash_connection spec end end - def resolve_string_connection(spec) - # Rails has historically accepted a string to mean either - # an environment key or a URL spec, so we have deprecated - # this ambiguous behaviour and in the future this function - # can be removed in favor of resolve_url_connection. - if configurations.key?(spec) || spec !~ /:/ - ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \ - "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead" - resolve_symbol_connection(spec) - else - resolve_url_connection(spec) - end - end - - # Takes the environment such as `:production` or `:development`. + # Takes the environment such as +:production+ or +:development+. # This requires that the @configurations was initialized with a key that # matches. # @@ -250,7 +236,7 @@ module ActiveRecord # Connection details inside of the "url" key win any merge conflicts def resolve_hash_connection(spec) if spec["url"] && spec["url"] !~ /^jdbc:/ - connection_hash = resolve_string_connection(spec.delete("url")) + connection_hash = resolve_url_connection(spec.delete("url")) spec.merge!(connection_hash) end spec diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 0a14cdfe89..fac6f81540 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -29,7 +29,7 @@ module ActiveRecord module ConnectionAdapters class Mysql2Adapter < AbstractMysqlAdapter - ADAPTER_NAME = 'Mysql2' + ADAPTER_NAME = 'Mysql2'.freeze def initialize(connection, logger, connection_options, config) super @@ -39,7 +39,7 @@ module ActiveRecord MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191 def initialize_schema_migrations_table - if @config[:encoding] == 'utf8mb4' + if charset == 'utf8mb4' ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4) else ActiveRecord::SchemaMigration.create_table @@ -66,21 +66,17 @@ module ActiveRecord exception.error_number if exception.respond_to?(:error_number) end + #-- # QUOTING ================================================== + #++ def quote_string(string) @connection.escape(string) end - def quoted_date(value) - if value.acts_like?(:time) && value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - + #-- # CONNECTION MANAGEMENT ==================================== + #++ def active? return false unless @connection @@ -104,7 +100,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds.dup)}" @@ -232,11 +230,6 @@ module ActiveRecord alias exec_without_stmt exec_query - # Returns an ActiveRecord::Result instance. - def select(sql, name = nil, binds = []) - exec_query(sql, name) - end - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) super id_value || @connection.last_id @@ -269,8 +262,8 @@ module ActiveRecord super end - def version - @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + def full_version + @full_version ||= @connection.info[:version] end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 909bba8c7d..16f50fc594 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -58,7 +58,7 @@ module ActiveRecord # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as `SET @@SESSION.key = value` on each database connection. Use the value `:default` to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html). + # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html). # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. @@ -66,7 +66,7 @@ module ActiveRecord # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. # class MysqlAdapter < AbstractMysqlAdapter - ADAPTER_NAME = 'MySQL' + ADAPTER_NAME = 'MySQL'.freeze class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max = 1000) @@ -88,7 +88,7 @@ module ActiveRecord end def clear - cache.values.each do |hash| + cache.each_value do |hash| hash[:stmt].close end cache.clear @@ -137,7 +137,9 @@ module ActiveRecord @connection.quote(string) end + #-- # CONNECTION MANAGEMENT ==================================== + #++ def active? if @connection.respond_to?(:stat) @@ -178,7 +180,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def select_rows(sql, name = nil, binds = []) @connection.query_with_result = true @@ -263,7 +267,7 @@ module ActiveRecord end module Fields # :nodoc: - class DateTime < Type::DateTime + class DateTime < Type::DateTime # :nodoc: def cast_value(value) if Mysql::Time === value new_time( @@ -280,7 +284,7 @@ module ActiveRecord end end - class Time < Type::Time + class Time < Type::Time # :nodoc: def cast_value(value) if Mysql::Time === value new_time( @@ -391,11 +395,9 @@ module ActiveRecord def exec_stmt(sql, name, binds) cache = {} - type_casted_binds = binds.map { |col, val| - [col, type_cast(val, col)] - } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - log(sql, name, type_casted_binds) do + log(sql, name, binds) do if binds.empty? stmt = @connection.prepare(sql) else @@ -406,10 +408,10 @@ module ActiveRecord end begin - stmt.execute(*type_casted_binds.map { |_, val| val }) + stmt.execute(*type_casted_binds) rescue Mysql::Error => e # Older versions of MySQL leave the prepared statement in a bad - # place when an error occurs. To support older mysql versions, we + # place when an error occurs. To support older MySQL versions, we # need to close the statement and delete the statement from the # cache. stmt.close @@ -419,9 +421,7 @@ module ActiveRecord cols = nil if metadata = stmt.result_metadata - cols = cache[:cols] ||= metadata.fetch_fields.map { |field| - field.name - } + cols = cache[:cols] ||= metadata.fetch_fields.map(&:name) metadata.free end @@ -465,14 +465,14 @@ module ActiveRecord def select(sql, name = nil, binds = []) @connection.query_with_result = true - rows = exec_query(sql, name, binds) + rows = super @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end - # Returns the version of the connected MySQL server. - def version - @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + # Returns the full version of the connected MySQL server. + def full_version + @full_version ||= @connection.server_info end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb deleted file mode 100644 index bb54de05c8..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ /dev/null @@ -1,105 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module Cast # :nodoc: - def point_to_string(point) # :nodoc: - "(#{point[0]},#{point[1]})" - end - - def hstore_to_string(object, array_member = false) # :nodoc: - if Hash === object - string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',') - string = escape_hstore(string) if array_member - string - else - object - end - end - - def string_to_hstore(string) # :nodoc: - if string.nil? - nil - elsif String === string - Hash[string.scan(HstorePair).map { |k, v| - v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - [k, v] - }] - else - string - end - end - - def json_to_string(object) # :nodoc: - if Hash === object || Array === object - ActiveSupport::JSON.encode(object) - else - object - end - end - - def array_to_string(value, column, adapter) # :nodoc: - casted_values = value.map do |val| - if String === val - if val == "NULL" - "\"#{val}\"" - else - quote_and_escape(adapter.type_cast(val, column, true)) - end - else - adapter.type_cast(val, column, true) - end - end - "{#{casted_values.join(',')}}" - end - - def range_to_string(object) # :nodoc: - from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin - to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end - "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}" - end - - def string_to_json(string) # :nodoc: - if String === string - ActiveSupport::JSON.decode(string) - else - string - end - end - - private - - HstorePair = begin - quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ - unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ - /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ - end - - def escape_hstore(value) - if value.nil? - 'NULL' - else - if value == "" - '""' - else - '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') - end - end - end - - ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays - - def quote_and_escape(value) - case value - when "NULL", Numeric - value - else - value = value.gsub(/\\/, ARRAY_ESCAPE) - value.gsub!(/"/,"\\\"") - "\"#{value}\"" - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 847fd4dded..0eb4fb468c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -1,23 +1,12 @@ -require 'active_record/connection_adapters/postgresql/cast' - module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: - extend PostgreSQL::Cast - - attr_accessor :array - - def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) - if sql_type =~ /\[\]$/ - @array = true - super(name, default, cast_type, sql_type[0..sql_type.length - 3], null) - else - @array = false - super(name, default, cast_type, sql_type, null) - end + delegate :array, :oid, :fmod, to: :sql_type_metadata + alias :array? :array - @default_function = default_function + def serial? + default_function && default_function =~ /\Anextval\(.*\)\z/ end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 89a7257d77..11d3f5301a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -156,10 +156,6 @@ module ActiveRecord end end - def substitute_at(column, index) - Arel::Nodes::BindParam.new "$#{index + 1}" - end - def exec_query(sql, name = 'SQL', binds = []) execute_and_clear(sql, name, binds) do |result| types = {} @@ -227,7 +223,7 @@ module ActiveRecord end # Aborts a transaction. - def rollback_db_transaction + def exec_rollback_db_transaction execute "ROLLBACK" end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 33a98b4fcb..92349e2f9b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,26 +1,22 @@ -require 'active_record/connection_adapters/postgresql/oid/infinity' - require 'active_record/connection_adapters/postgresql/oid/array' require 'active_record/connection_adapters/postgresql/oid/bit' require 'active_record/connection_adapters/postgresql/oid/bit_varying' require 'active_record/connection_adapters/postgresql/oid/bytea' require 'active_record/connection_adapters/postgresql/oid/cidr' -require 'active_record/connection_adapters/postgresql/oid/date' require 'active_record/connection_adapters/postgresql/oid/date_time' require 'active_record/connection_adapters/postgresql/oid/decimal' require 'active_record/connection_adapters/postgresql/oid/enum' -require 'active_record/connection_adapters/postgresql/oid/float' require 'active_record/connection_adapters/postgresql/oid/hstore' require 'active_record/connection_adapters/postgresql/oid/inet' -require 'active_record/connection_adapters/postgresql/oid/integer' require 'active_record/connection_adapters/postgresql/oid/json' +require 'active_record/connection_adapters/postgresql/oid/jsonb' require 'active_record/connection_adapters/postgresql/oid/money' require 'active_record/connection_adapters/postgresql/oid/point' require 'active_record/connection_adapters/postgresql/oid/range' require 'active_record/connection_adapters/postgresql/oid/specialized_string' -require 'active_record/connection_adapters/postgresql/oid/time' require 'active_record/connection_adapters/postgresql/oid/uuid' require 'active_record/connection_adapters/postgresql/oid/vector' +require 'active_record/connection_adapters/postgresql/oid/xml' require 'active_record/connection_adapters/postgresql/oid/type_map_initializer' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index 4e7d472d97..fb4e0de2a8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -2,36 +2,56 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Array < Type::Value - attr_reader :subtype - delegate :type, to: :subtype + class Array < Type::Value # :nodoc: + include Type::Helpers::Mutable - def initialize(subtype) + # Loads pg_array_parser if available. String parsing can be + # performed quicker by a native extension, which will not create + # a large amount of Ruby objects that will need to be garbage + # collected. pg_array_parser has a C and Java extension + begin + require 'pg_array_parser' + include PgArrayParser + rescue LoadError + require 'active_record/connection_adapters/postgresql/array_parser' + include PostgreSQL::ArrayParser + end + + attr_reader :subtype, :delimiter + delegate :type, :user_input_in_time_zone, to: :subtype + + def initialize(subtype, delimiter = ',') @subtype = subtype + @delimiter = delimiter end - def type_cast_from_database(value) + def deserialize(value) if value.is_a?(::String) - type_cast_array(parse_pg_array(value), :type_cast_from_database) + type_cast_array(parse_pg_array(value), :deserialize) else super end end - def type_cast_from_user(value) - type_cast_array(value, :type_cast_from_user) + def cast(value) + if value.is_a?(::String) + value = parse_pg_array(value) + end + type_cast_array(value, :cast) end - # Loads pg_array_parser if available. String parsing can be - # performed quicker by a native extension, which will not create - # a large amount of Ruby objects that will need to be garbage - # collected. pg_array_parser has a C and Java extension - begin - require 'pg_array_parser' - include PgArrayParser - rescue LoadError - require 'active_record/connection_adapters/postgresql/array_parser' - include PostgreSQL::ArrayParser + def serialize(value) + if value.is_a?(::Array) + cast_value_for_database(value) + else + super + end + end + + def ==(other) + other.is_a?(Array) && + subtype == other.subtype && + delimiter == other.delimiter end private @@ -43,6 +63,41 @@ module ActiveRecord @subtype.public_send(method, value) end end + + def cast_value_for_database(value) + if value.is_a?(::Array) + casted_values = value.map { |item| cast_value_for_database(item) } + "{#{casted_values.join(delimiter)}}" + else + quote_and_escape(subtype.serialize(value)) + end + end + + ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays + + def quote_and_escape(value) + case value + when ::String + if string_requires_quoting?(value) + value = value.gsub(/\\/, ARRAY_ESCAPE) + value.gsub!(/"/,"\\\"") + %("#{value}") + else + value + end + when nil then "NULL" + else value + end + end + + # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO + # for a list of all cases in which strings will be quoted. + def string_requires_quoting?(string) + string.empty? || + string == "NULL" || + string =~ /[\{\}"\\\s]/ || + string.include?(delimiter) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 3073f8ff30..ea0fa2517f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -2,12 +2,12 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Bit < Type::Value + class Bit < Type::Value # :nodoc: def type :bit end - def type_cast(value) + def cast(value) if ::String === value case value when /^0x/i @@ -19,6 +19,32 @@ module ActiveRecord value end end + + def serialize(value) + Data.new(super) if value + end + + class Data + def initialize(value) + @value = value + end + + def to_s + value + end + + def binary? + /\A[01]*\Z/ === value + end + + def hex? + /\A[0-9A-F]*\Z/i === value + end + + protected + + attr_reader :value + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb index 054af285bb..4c21097d48 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class BitVarying < OID::Bit + class BitVarying < OID::Bit # :nodoc: def type :bit_varying end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 89b203d2b1..8f9d6e7f9b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -2,9 +2,10 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Bytea < Type::Binary - def type_cast_from_database(value) + class Bytea < Type::Binary # :nodoc: + def deserialize(value) return if value.nil? + return value.to_s if value.is_a?(Type::Binary::Data) PGconn.unescape_bytea(super) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb index 534961a414..eeccb09bdf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Cidr < Type::Value + class Cidr < Type::Value # :nodoc: def type :cidr end @@ -12,15 +12,15 @@ module ActiveRecord # If the subnet mask is equal to /32, don't output it if subnet_mask == (2**32 - 1) - "\"#{value.to_s}\"" + "\"#{value}\"" else - "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\"" + "\"#{value}/#{subnet_mask.to_s(2).count('1')}\"" end end - def type_cast_for_database(value) + def serialize(value) if IPAddr === value - "#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + "#{value}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" else value end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb deleted file mode 100644 index 3c30ad5fec..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Date < Type::Date - include Infinity - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb index 34e2276dd1..2c04c46131 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -2,9 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class DateTime < Type::DateTime - include Infinity - + class DateTime < Type::DateTime # :nodoc: def cast_value(value) if value.is_a?(::String) case value diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb index ed4b8911d9..43d22c8daf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Decimal < Type::Decimal + class Decimal < Type::Decimal # :nodoc: def infinity(options = {}) BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb index 5fed8b0f89..b3b610a5f6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -2,12 +2,12 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Enum < Type::Value + class Enum < Type::Value # :nodoc: def type :enum end - def type_cast(value) + def cast(value) value.to_s end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb deleted file mode 100644 index 77dd97e140..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb +++ /dev/null @@ -1,20 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Float < Type::Float - include Infinity - - def cast_value(value) - case value - when 'Infinity' then ::Float::INFINITY - when '-Infinity' then -::Float::INFINITY - when 'NaN' then ::Float::NAN - else value.to_f - end - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index 0dd4b65333..9270fc9f21 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -2,24 +2,56 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Hstore < Type::Value - include Type::Mutable + class Hstore < Type::Value # :nodoc: + include Type::Helpers::Mutable def type :hstore end - def type_cast_from_database(value) - ConnectionAdapters::PostgreSQLColumn.string_to_hstore(value) + def deserialize(value) + if value.is_a?(::String) + ::Hash[value.scan(HstorePair).map { |k, v| + v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + [k, v] + }] + else + value + end end - def type_cast_for_database(value) - ConnectionAdapters::PostgreSQLColumn.hstore_to_string(value) + def serialize(value) + if value.is_a?(::Hash) + value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ') + else + value + end end def accessor ActiveRecord::Store::StringKeyedHashAccessor end + + private + + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + + def escape_hstore(value) + if value.nil? + 'NULL' + else + if value == "" + '""' + else + '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + end + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb index 7ed8f5f031..96486fa65b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Inet < Cidr + class Inet < Cidr # :nodoc: def type :inet end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb deleted file mode 100644 index d438ffa140..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - module Infinity - def infinity(options = {}) - options[:negative] ? -::Float::INFINITY : ::Float::INFINITY - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb deleted file mode 100644 index 388d3dd9ed..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Integer < Type::Integer - include Infinity - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index d1347c7bb5..8e1256baad 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,19 +2,27 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Json < Type::Value - include Type::Mutable + class Json < Type::Value # :nodoc: + include Type::Helpers::Mutable def type :json end - def type_cast_from_database(value) - ConnectionAdapters::PostgreSQLColumn.string_to_json(value) + def deserialize(value) + if value.is_a?(::String) + ::ActiveSupport::JSON.decode(value) rescue nil + else + value + end end - def type_cast_for_database(value) - ConnectionAdapters::PostgreSQLColumn.json_to_string(value) + def serialize(value) + if value.is_a?(::Array) || value.is_a?(::Hash) + ::ActiveSupport::JSON.encode(value) + else + value + end end def accessor diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb new file mode 100644 index 0000000000..afc9383f91 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Jsonb < Json # :nodoc: + def type + :jsonb + end + + def changed_in_place?(raw_old_value, new_value) + # Postgres does not preserve insignificant whitespaces when + # roundtripping jsonb columns. This causes some false positives for + # the comparison here. Therefore, we need to parse and re-dump the + # raw value here to ensure the insignificant whitespaces are + # consistent with our encoder's output. + raw_old_value = serialize(deserialize(raw_old_value)) + super(raw_old_value, new_value) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index d25eb256c2..2163674019 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -2,9 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Money < Type::Decimal - include Infinity - + class Money < Type::Decimal # :nodoc: class_attribute :precision def type diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index 9007bfb178..bf565bcf47 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -2,21 +2,40 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Point < Type::Value + class Point < Type::Value # :nodoc: + include Type::Helpers::Mutable + def type :point end - def type_cast(value) - if ::String === value + def cast(value) + case value + when ::String if value[0] == '(' && value[-1] == ')' value = value[1...-1] end - value.split(',').map{ |v| Float(v) } + cast(value.split(',')) + when ::Array + value.map { |v| Float(v) } else value end end + + def serialize(value) + if value.is_a?(::Array) + "(#{number_for_point(value[0])},#{number_for_point(value[1])})" + else + super + end + end + + private + + def number_for_point(number) + number.to_s.gsub(/\.0$/, '') + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index c289ba8980..fc201f8fb9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -1,37 +1,21 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Range < Type::Value + class Range < Type::Value # :nodoc: attr_reader :subtype, :type - def initialize(subtype, type) + def initialize(subtype, type = :range) @subtype = subtype @type = type end - def extract_bounds(value) - from, to = value[1..-2].split(',') - { - from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, - to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, - exclude_start: (value[0] == '('), - exclude_end: (value[-1] == ')') - } - end - - def infinity?(value) - value.respond_to?(:infinite?) && value.infinite? - end - def type_cast_for_schema(value) value.inspect.gsub('Infinity', '::Float::INFINITY') end - def type_cast_single(value) - infinity?(value) ? value : @subtype.type_cast_from_database(value) - end - def cast_value(value) return if value == 'empty' return value if value.is_a?(::Range) @@ -41,18 +25,60 @@ module ActiveRecord to = type_cast_single extracted[:to] if !infinity?(from) && extracted[:exclude_start] - if from.respond_to?(:succ) - from = from.succ - ActiveSupport::Deprecation.warn <<-MESSAGE -Excluding the beginning of a Range is only partialy supported through `#succ`. -This is not reliable and will be removed in the future. - MESSAGE - else - raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" - end + raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" end ::Range.new(from, to, extracted[:exclude_end]) end + + def serialize(value) + if value.is_a?(::Range) + from = type_cast_single_for_database(value.begin) + to = type_cast_single_for_database(value.end) + "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}" + else + super + end + end + + def ==(other) + other.is_a?(Range) && + other.subtype == subtype && + other.type == type + end + + private + + def type_cast_single(value) + infinity?(value) ? value : @subtype.deserialize(value) + end + + def type_cast_single_for_database(value) + infinity?(value) ? '' : @subtype.serialize(value) + end + + def extract_bounds(value) + from, to = value[1..-2].split(',') + { + from: (value[1] == ',' || from == '-infinity') ? infinity(negative: true) : from, + to: (value[-2] == ',' || to == 'infinity') ? infinity : to, + exclude_start: (value[0] == '('), + exclude_end: (value[-1] == ')') + } + end + + def infinity(negative: false) + if subtype.respond_to?(:infinity) + subtype.infinity(negative: negative) + elsif negative + -::Float::INFINITY + else + ::Float::INFINITY + end + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index 7b1ca16bc4..2d2fede4e8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -2,16 +2,12 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class SpecializedString < Type::String + class SpecializedString < Type::String # :nodoc: attr_reader :type def initialize(type) @type = type end - - def text? - false - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb deleted file mode 100644 index ea1f599b0f..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Time < Type::Time - include Infinity - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 28f7a4eafb..9b3de41fab 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -4,7 +4,7 @@ module ActiveRecord module OID # :nodoc: # This class uses the data from PostgreSQL pg_type table to build # the OID -> Type mapping. - # - OID is and integer representing the type. + # - OID is an integer representing the type. # - Type is an OID::Type object. # This class has side effects on the +store+ passed during initialization. class TypeMapInitializer # :nodoc: @@ -39,14 +39,14 @@ module ActiveRecord end def register_array_type(row) - if subtype = @store.lookup(row['typelem'].to_i) - register row['oid'], OID::Array.new(subtype) + register_with_subtype(row['oid'], row['typelem'].to_i) do |subtype| + OID::Array.new(subtype, row['typdelim']) end end def register_range_type(row) - if subtype = @store.lookup(row['rngsubtype'].to_i) - register row['oid'], OID::Range.new(subtype, row['typname'].to_sym) + register_with_subtype(row['oid'], row['rngsubtype'].to_i) do |subtype| + OID::Range.new(subtype, row['typname'].to_sym) end end @@ -64,9 +64,13 @@ module ActiveRecord end end - def register(oid, oid_type) - oid = assert_valid_registration(oid, oid_type) - @store.register_type(oid, oid_type) + def register(oid, oid_type = nil, &block) + oid = assert_valid_registration(oid, oid_type || block) + if block_given? + @store.register_type(oid, &block) + else + @store.register_type(oid, oid_type) + end end def alias_type(oid, target) @@ -74,6 +78,14 @@ module ActiveRecord @store.alias_type(oid, target) end + def register_with_subtype(oid, target_oid) + if @store.key?(target_oid) + register(oid) do |_, *args| + yield @store.lookup(target_oid, *args) + end + end + end + def assert_valid_registration(oid, oid_type) raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? oid.to_i diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index 0ed5491887..5e839228e9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -2,13 +2,17 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Uuid < Type::Value + class Uuid < Type::Value # :nodoc: + ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x + + alias_method :serialize, :deserialize + def type :uuid end - def type_cast(value) - value.presence + def cast(value) + value.to_s[ACCEPTABLE_UUID, 0] end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb index 2f7d1be197..b26e876b54 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Vector < Type::Value + class Vector < Type::Value # :nodoc: attr_reader :delim, :subtype # +delim+ corresponds to the `typdelim` column in the pg_types @@ -16,7 +16,7 @@ module ActiveRecord # FIXME: this should probably split on +delim+ and use +subtype+ # to cast the values. Unfortunately, the current Rails behavior # is to just return the string. - def type_cast(value) + def cast(value) value end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb new file mode 100644 index 0000000000..d40d837cee --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb @@ -0,0 +1,28 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Xml < Type::String # :nodoc: + def type + :xml + end + + def serialize(value) + return unless value + Data.new(super) + end + + class Data # :nodoc: + def initialize(value) + @value = value + end + + def to_s + @value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 3cf40e6cd4..b7755c4593 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -14,107 +14,6 @@ module ActiveRecord @connection.unescape_bytea(value) if value end - # Quotes PostgreSQL-specific data types for SQL input. - def quote(value, column = nil) #:nodoc: - return super unless column - - sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale) - - case value - when Range - if /range$/ =~ sql_type - "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}" - else - super - end - when Array - case sql_type - when 'point' then super(PostgreSQLColumn.point_to_string(value)) - when 'json' then super(PostgreSQLColumn.json_to_string(value)) - else - if column.array - "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'" - else - super - end - end - when Hash - case sql_type - when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) - when 'json' then super(PostgreSQLColumn.json_to_string(value), column) - else super - end - when Float - if value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" - elsif value.infinite? || value.nan? - "'#{value.to_s}'" - else - super - end - when Numeric - if sql_type == 'money' || [:string, :text].include?(column.type) - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" - else - super - end - when String - case sql_type - when 'xml' then "xml '#{quote_string(value)}'" - when /^bit/ - case value - when /^[01]*$/ then "B'#{value}'" # Bit-string notation - when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation - end - else - super - end - else - super - end - end - - def type_cast(value, column, array_member = false) - return super(value, column) unless column - - case value - when Range - if /range$/ =~ column.sql_type - PostgreSQLColumn.range_to_string(value) - else - super(value, column) - end - when NilClass - if column.array && array_member - 'NULL' - elsif column.array - value - else - super(value, column) - end - when Array - case column.sql_type - when 'point' then PostgreSQLColumn.point_to_string(value) - when 'json' then PostgreSQLColumn.json_to_string(value) - else - if column.array - PostgreSQLColumn.array_to_string(value, column, self) - else - super(value, column) - end - end - when Hash - case column.sql_type - when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member) - when 'json' then PostgreSQLColumn.json_to_string(value) - else super(value, column) - end - else - super(value, column) - end - end - # Quotes strings for use in SQL input. def quote_string(s) #:nodoc: @connection.escape(s) @@ -144,43 +43,64 @@ module ActiveRecord # Quote date/time values for use in SQL input. Includes microseconds # if the value is a Time responding to usec. def quoted_date(value) #:nodoc: - result = super - if value.acts_like?(:time) && value.respond_to?(:usec) - result = "#{result}.#{sprintf("%06d", value.usec)}" - end - if value.year <= 0 bce_year = format("%04d", -value.year + 1) - result = result.sub(/^-?\d+/, bce_year) + " BC" + super.sub(/^-?\d+/, bce_year) + " BC" + else + super end - result end # Does not quote function default values for UUID columns - def quote_default_value(value, column) #:nodoc: + def quote_default_expression(value, column) #:nodoc: if column.type == :uuid && value =~ /\(\)/ value + elsif column.respond_to?(:array?) + value = type_cast_from_column(column, value) + quote(value) else - quote(value, column) + super end end + def lookup_cast_type_from_column(column) # :nodoc: + type_map.lookup(column.oid, column.fmod, column.sql_type) + end + private def _quote(value) - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data "'#{escape_bytea(value.to_s)}'" + when OID::Xml::Data + "xml '#{quote_string(value.to_s)}'" + when OID::Bit::Data + if value.binary? + "B'#{value}'" + elsif value.hex? + "X'#{value}'" + end + when Float + if value.infinite? || value.nan? + "'#{value}'" + else + super + end else super end end def _type_cast(value) - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data # Return a bind param hash with format as binary. # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc # for more information { value: value.to_s, format: 1 } + when OID::Xml::Data, OID::Bit::Data + value.to_s else super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 0867e5ef54..b9078d4c86 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -64,6 +64,10 @@ module ActiveRecord column(name, :json, options) end + def jsonb(name, options = {}) + column(name, :jsonb, options) + end + def citext(name, options = {}) column(name, :citext, options) end @@ -72,15 +76,15 @@ module ActiveRecord column(name, :point, options) end - def bit(name, options) + def bit(name, options = {}) column(name, :bit, options) end - def bit_varying(name, options) + def bit_varying(name, options = {}) column(name, :bit_varying, options) end - def money(name, options) + def money(name, options = {}) column(name, :money, options) end end @@ -121,18 +125,14 @@ module ActiveRecord # a record (as primary keys cannot be +nil+). This might be done via the # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. def primary_key(name, type = :primary_key, options = {}) - return super unless type == :uuid - options[:default] = options.fetch(:default, 'uuid_generate_v4()') - options[:primary_key] = true - column name, type, options + options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid + super end - def column(name, type = nil, options = {}) - super - column = self[name] + def new_column_definition(name, type, options) # :nodoc: + column = super column.array = options[:array] - - self + column end private diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index b2aeb3a058..81fde18f64 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -4,32 +4,10 @@ module ActiveRecord class SchemaCreation < AbstractAdapter::SchemaCreation private - def visit_AddColumn(o) - sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) - sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}" - add_column_options!(sql, column_options(o)) - end - def visit_ColumnDefinition(o) - sql = super - if o.primary_key? && o.type != :primary_key - sql << " PRIMARY KEY " - add_column_options!(sql, column_options(o)) - end - sql - end - - def add_column_options!(sql, options) - if options[:array] || options[:column].try(:array) - sql << '[]' - end - - column = options.fetch(:column) { return super } - if column.type == :uuid && options[:default] =~ /\(\)/ - sql << " DEFAULT #{options[:default]}" - else - super - end + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) + o.sql_type << '[]' if o.array + super end end @@ -52,8 +30,8 @@ module ActiveRecord def create_database(name, options = {}) options = { encoding: 'utf8' }.merge!(options.symbolize_keys) - option_string = options.sum do |key, value| - case key + option_string = options.inject("") do |memo, (key, value)| + memo += case key when :owner " OWNER = \"#{value}\"" when :template @@ -110,6 +88,10 @@ module ActiveRecord SQL end + def drop_table(table_name, options = {}) + execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" + end + # Returns true if schema exists. def schema_exists?(name) exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 @@ -178,15 +160,17 @@ module ActiveRecord def columns(table_name) # Limit, precision, and scale are all handled by the superclass. column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| - oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type) - default_value = extract_value_from_default(oid, default) + oid = oid.to_i + fmod = fmod.to_i + type_metadata = fetch_type_metadata(column_name, type, oid, fmod) + default_value = extract_value_from_default(default) default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, oid, type, notnull == 'f', default_function) + new_column(column_name, default_value, type_metadata, notnull == 'f', default_function) end end - def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc: - PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function) + def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil) # :nodoc: + PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function) end # Returns the current database name. @@ -273,9 +257,9 @@ module ActiveRecord def default_sequence_name(table_name, pk = nil) #:nodoc: result = serial_sequence(table_name, pk || 'id') return nil unless result - Utils.extract_schema_qualified_name(result) + Utils.extract_schema_qualified_name(result).to_s rescue ActiveRecord::StatementInvalid - PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq") + PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq").to_s end def serial_sequence(table, column) @@ -285,6 +269,23 @@ module ActiveRecord result.rows.first.first end + # Sets the sequence of a table's primary key to the specified value. + def set_pk_sequence!(table, value) #:nodoc: + pk, sequence = pk_and_sequence_for(table) + + if pk + if sequence + quoted_sequence = quote_table_name(sequence) + + select_value <<-end_sql, 'SCHEMA' + SELECT setval('#{quoted_sequence}', #{value}) + end_sql + else + @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger + end + end + end + # Resets the sequence of a table's primary key to the maximum value. def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: unless pk and sequence @@ -363,20 +364,20 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table) - row = exec_query(<<-end_sql, 'SCHEMA').rows.first + pks = exec_query(<<-end_sql, 'SCHEMA').rows SELECT attr.attname FROM pg_attribute attr - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '#{quote_table_name(table)}'::regclass end_sql - - row && row.first + return nil unless pks.count == 1 + pks[0][0] end # Renames a table. - # Also renames a table's primary key sequence if the sequence name matches the - # Active Record default. + # Also renames a table's primary key sequence if the sequence name exists and + # matches the Active Record default. # # Example: # rename_table('octopuses', 'octopi') @@ -384,9 +385,12 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq.identifier == "#{table_name}_#{pk}_seq" + if seq && seq.identifier == "#{table_name}_#{pk}_seq" new_seq = "#{new_name}_#{pk}_seq" + idx = "#{table_name}_pkey" + new_idx = "#{new_name}_pkey" execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" + execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}" end rename_table_indexes(table_name, new_name) @@ -405,7 +409,12 @@ module ActiveRecord quoted_table_name = quote_table_name(table_name) sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale]) sql_type << "[]" if options[:array] - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" + sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" + sql << " USING #{options[:using]}" if options[:using] + if options[:cast_as] + sql << " USING CAST(#{quote_column_name(column_name)} AS #{type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale])})" + end + execute sql change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) @@ -415,15 +424,23 @@ module ActiveRecord def change_column_default(table_name, column_name, default) clear_cache! column = column_for(table_name, column_name) + return unless column - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote_default_value(default, column)}" if column + alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" + if default.nil? + # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will + # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". + execute alter_column_query % "DROP DEFAULT" + else + execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}" + end end def change_column_null(table_name, column_name, null, default = nil) clear_cache! unless null || default.nil? column = column_for(table_name, column_name) - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column end execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") end @@ -445,9 +462,48 @@ module ActiveRecord end def rename_index(table_name, old_name, new_name) + validate_index_length!(table_name, new_name) + execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete + FROM pg_constraint c + JOIN pg_class t1 ON c.conrelid = t1.oid + JOIN pg_class t2 ON c.confrelid = t2.oid + JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid + JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid + JOIN pg_namespace t3 ON c.connamespace = t3.oid + WHERE c.contype = 'f' + AND t1.relname = #{quote(table_name)} + AND t3.nspname = ANY (current_schemas(false)) + ORDER BY c.conname + SQL + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_delete] = extract_foreign_key_action(row['on_delete']) + options[:on_update] = extract_foreign_key_action(row['on_update']) + + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when 'c'; :cascade + when 'n'; :nullify + when 'r'; :restrict + end + end + def index_name_length 63 end @@ -497,11 +553,24 @@ module ActiveRecord # Convert Arel node to string s = s.to_sql unless s.is_a?(String) # Remove any ASC/DESC modifiers - s.gsub(/\s+(?:ASC|DESC)?\s*(?:NULLS\s+(?:FIRST|LAST)\s*)?/i, '') + s.gsub(/\s+(?:ASC|DESC)\b/i, '') + .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '') }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } [super, *order_columns].join(', ') end + + def fetch_type_metadata(column_name, sql_type, oid, fmod) + cast_type = get_oid_type(oid, fmod, column_name, sql_type) + simple_type = SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb new file mode 100644 index 0000000000..58715978f7 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -0,0 +1,35 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) + attr_reader :oid, :fmod, :array + + def initialize(type_metadata, oid: nil, fmod: nil) + super(type_metadata) + @type_metadata = type_metadata + @oid = oid + @fmod = fmod + @array = /\[\]$/ === type_metadata.sql_type + end + + def sql_type + super.gsub(/\[\]$/, "") + end + + def ==(other) + other.is_a?(PostgreSQLTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, @type_metadata, oid, fmod] + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb index 0290bcb48c..9a0b80d7d3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -18,7 +18,11 @@ module ActiveRecord end def quoted - parts.map { |p| PGconn.quote_ident(p) }.join SEPARATOR + if schema + PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier) + else + PGconn.quote_ident(identifier) + end end def ==(o) @@ -32,8 +36,11 @@ module ActiveRecord protected def unquote(part) - return unless part - part.gsub(/(^"|"$)/,'') + if part && part.start_with?('"') + part[1..-2] + else + part + end end def parts @@ -57,7 +64,11 @@ module ActiveRecord # * <tt>"schema_name".table_name</tt> # * <tt>"schema.name"."table name"</tt> def extract_schema_qualified_name(string) - table, schema = string.scan(/[^".\s]+|"[^"]*"/)[0..1].reverse + schema, table = string.scan(/[^".\s]+|"[^"]*"/) + if table.nil? + table = schema + schema = nil + end PostgreSQL::Name.new(schema, table) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 71b05cdbae..5a887ea529 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,19 +1,19 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_record/connection_adapters/statement_pool' - -require 'active_record/connection_adapters/postgresql/utils' -require 'active_record/connection_adapters/postgresql/column' -require 'active_record/connection_adapters/postgresql/oid' -require 'active_record/connection_adapters/postgresql/quoting' -require 'active_record/connection_adapters/postgresql/referential_integrity' -require 'active_record/connection_adapters/postgresql/schema_definitions' -require 'active_record/connection_adapters/postgresql/schema_statements' -require 'active_record/connection_adapters/postgresql/database_statements' +require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/postgresql/column" +require "active_record/connection_adapters/postgresql/database_statements" +require "active_record/connection_adapters/postgresql/oid" +require "active_record/connection_adapters/postgresql/quoting" +require "active_record/connection_adapters/postgresql/referential_integrity" +require "active_record/connection_adapters/postgresql/schema_definitions" +require "active_record/connection_adapters/postgresql/schema_statements" +require "active_record/connection_adapters/postgresql/type_metadata" +require "active_record/connection_adapters/postgresql/utils" +require "active_record/connection_adapters/statement_pool" require 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values -gem 'pg', '~> 0.11' +gem 'pg', '~> 0.15' require 'pg' require 'ipaddr' @@ -64,7 +64,7 @@ module ActiveRecord # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. # * <tt>:variables</tt> - An optional hash of additional parameters that # will be used in <tt>SET SESSION key = val</tt> calls on the connection. - # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT</tt> statements + # * <tt>:insert_returning</tt> - An optional boolean to control the use of <tt>RETURNING</tt> for <tt>INSERT</tt> statements # defaults to true. # # Any further options are used as connection parameters to libpq. See @@ -74,10 +74,11 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter - ADAPTER_NAME = 'PostgreSQL' + ADAPTER_NAME = 'PostgreSQL'.freeze NATIVE_DATABASE_TYPES = { primary_key: "serial primary key", + bigserial: "bigserial", string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, @@ -94,6 +95,7 @@ module ActiveRecord int8range: { name: "int8range" }, binary: { name: "bytea" }, boolean: { name: "boolean" }, + bigint: { name: "bigint" }, xml: { name: "xml" }, tsvector: { name: "tsvector" }, hstore: { name: "hstore" }, @@ -102,6 +104,7 @@ module ActiveRecord macaddr: { name: "macaddr" }, uuid: { name: "uuid" }, json: { name: "json" }, + jsonb: { name: "jsonb" }, ltree: { name: "ltree" }, citext: { name: "citext" }, point: { name: "point" }, @@ -118,25 +121,35 @@ module ActiveRecord include PostgreSQL::DatabaseStatements include Savepoints - # Returns 'PostgreSQL' as adapter name for identification purposes. - def adapter_name - ADAPTER_NAME - end - def schema_creation # :nodoc: PostgreSQL::SchemaCreation.new self end - # Adds `:array` option to the default set provided by the + def column_spec_for_primary_key(column) + spec = {} + if column.serial? + return unless column.sql_type == 'bigint' + spec[:id] = ':bigserial' + elsif column.type == :uuid + spec[:id] = ':uuid' + spec[:default] = column.default_function.inspect + else + spec[:id] = column.type.inspect + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + end + spec + end + + # Adds +:array+ option to the default set provided by the # AbstractAdapter - def prepare_column_options(column, types) # :nodoc: + def prepare_column_options(column) # :nodoc: spec = super - spec[:array] = 'true' if column.respond_to?(:array) && column.array + spec[:array] = 'true' if column.array? spec[:default] = "\"#{column.default_function}\"" if column.default_function spec end - # Adds `:array` as a valid migration key + # Adds +:array+ as a valid migration key def migration_keys super + [:array] end @@ -159,6 +172,14 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + + def supports_views? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -234,6 +255,8 @@ module ActiveRecord @table_alias_length = nil connect + add_pg_decoders + @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) @@ -252,6 +275,10 @@ module ActiveRecord @statements.clear end + def truncate(table_name, name = nil) + exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, [] + end + # Is this connection alive and ready for queries? def active? @connection.query 'SELECT 1' @@ -306,10 +333,6 @@ module ActiveRecord self.client_min_messages = old end - def supports_insert_with_returning? - true - end - def supports_ddl_transactions? true end @@ -348,14 +371,13 @@ module ActiveRecord if supports_extensions? res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", 'SCHEMA' - res.column_types['enabled'].type_cast_from_database res.rows.first.first + res.cast_values.first end end def extensions if supports_extensions? - res = exec_query "SELECT extname from pg_extension", "SCHEMA" - res.rows.map { |r| res.column_types['extname'].type_cast_from_database r.first } + exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values else super end @@ -384,6 +406,21 @@ module ActiveRecord PostgreSQL::Table.new(table_name, base) end + def lookup_cast_type(sql_type) # :nodoc: + oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i + super(oid) + end + + def column_name_for_operation(operation, node) # :nodoc: + OPERATION_ALIASES.fetch(operation) { operation.downcase } + end + + OPERATION_ALIASES = { # :nodoc: + "maximum" => "max", + "minimum" => "min", + "average" => "avg", + } + protected # Returns the version of the connected PostgreSQL server. @@ -424,11 +461,11 @@ module ActiveRecord end def initialize_type_map(m) # :nodoc: - register_class_with_limit m, 'int2', OID::Integer - m.alias_type 'int4', 'int2' - m.alias_type 'int8', 'int2' + register_class_with_limit m, 'int2', Type::Integer + register_class_with_limit m, 'int4', Type::Integer + register_class_with_limit m, 'int8', Type::Integer m.alias_type 'oid', 'int2' - m.register_type 'float4', OID::Float.new + m.register_type 'float4', Type::Float.new m.alias_type 'float8', 'float4' m.register_type 'text', Type::Text.new register_class_with_limit m, 'varchar', Type::String @@ -439,18 +476,19 @@ module ActiveRecord register_class_with_limit m, 'bit', OID::Bit register_class_with_limit m, 'varbit', OID::BitVarying m.alias_type 'timestamptz', 'timestamp' - m.register_type 'date', OID::Date.new - m.register_type 'time', OID::Time.new + m.register_type 'date', Type::Date.new + m.register_type 'time', Type::Time.new m.register_type 'money', OID::Money.new m.register_type 'bytea', OID::Bytea.new m.register_type 'point', OID::Point.new m.register_type 'hstore', OID::Hstore.new m.register_type 'json', OID::Json.new + m.register_type 'jsonb', OID::Jsonb.new m.register_type 'cidr', OID::Cidr.new m.register_type 'inet', OID::Inet.new m.register_type 'uuid', OID::Uuid.new - m.register_type 'xml', OID::SpecializedString.new(:xml) + m.register_type 'xml', OID::Xml.new m.register_type 'tsvector', OID::SpecializedString.new(:tsvector) m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) m.register_type 'citext', OID::SpecializedString.new(:citext) @@ -495,14 +533,17 @@ module ActiveRecord def extract_limit(sql_type) # :nodoc: case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - else super + when /^bigint/i, /^int8/i + 8 + when /^smallint/i + 2 + else + super end end # Extracts the value from a PostgreSQL column default definition. - def extract_value_from_default(oid, default) # :nodoc: + def extract_value_from_default(default) # :nodoc: case default # Quoted types when /\A[\(B]?'(.*)'::/m @@ -511,7 +552,7 @@ module ActiveRecord when 'true', 'false' default # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ + when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ $1 # Object identifier types when /\A-?\d+\z/ @@ -565,19 +606,15 @@ module ActiveRecord end def exec_no_cache(sql, name, binds) - log(sql, name, binds) { @connection.async_exec(sql) } + log(sql, name, binds) { @connection.async_exec(sql, []) } end def exec_cache(sql, name, binds) stmt_key = prepare_statement(sql) - type_casted_binds = binds.map { |col, val| - [col, type_cast(val, col)] - } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - log(sql, name, type_casted_binds, stmt_key) do - @connection.send_query_prepared(stmt_key, type_casted_binds.map { |_, val| val }) - @connection.block - @connection.get_last_result + log(sql, name, binds, stmt_key) do + @connection.exec_prepared(stmt_key, type_casted_binds) end rescue ActiveRecord::StatementInvalid => e pgerror = e.original_exception @@ -669,9 +706,9 @@ module ActiveRecord variables.map do |k, v| if v == ':default' || v == :default # Sets the value to the global or compile default - execute("SET SESSION #{k.to_s} TO DEFAULT", 'SCHEMA') + execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA') elsif !v.nil? - execute("SET SESSION #{k.to_s} TO #{quote(v)}", 'SCHEMA') + execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA') end end end @@ -689,12 +726,6 @@ module ActiveRecord exec_query("SELECT currval('#{sequence_name}')", 'SQL') end - # Executes a SELECT query and returns the results, performing any data type - # conversions that are required to be performed here instead of in PostgreSQLColumn. - def select(sql, name = nil, binds = []) - exec_query(sql, name, binds) - end - # Returns the list of a table's column names, data types, and default values. # # The underlying query is roughly: @@ -730,9 +761,76 @@ module ActiveRecord $1.strip if $1 end - def create_table_definition(name, temporary, options, as = nil) # :nodoc: + def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as end + + def can_perform_case_insensitive_comparison_for?(column) + @case_insensitive_cache ||= {} + @case_insensitive_cache[column.sql_type] ||= begin + sql = <<-end_sql + SELECT exists( + SELECT * FROM pg_proc + INNER JOIN pg_cast + ON casttarget::text::oidvector = proargtypes + WHERE proname = 'lower' + AND castsource = '#{column.sql_type}'::regtype::oid + ) + end_sql + execute_and_clear(sql, "SCHEMA", []) do |result| + result.getvalue(0, 0) == 't' + end + end + end + + def add_pg_decoders + coders_by_name = { + 'int2' => PG::TextDecoder::Integer, + 'int4' => PG::TextDecoder::Integer, + 'int8' => PG::TextDecoder::Integer, + 'oid' => PG::TextDecoder::Integer, + 'float4' => PG::TextDecoder::Float, + 'float8' => PG::TextDecoder::Float, + 'bool' => PG::TextDecoder::Boolean, + } + query = <<-SQL + SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype + FROM pg_type as t + SQL + coders = execute_and_clear(query, "SCHEMA", []) do |result| + result + .map { |row| construct_coder(row, coders_by_name['typname']) } + .compact + end + + map = PG::TypeMapByOid.new + coders.each { |coder| map.add_coder(coder) } + @connection.type_map_for_results = map + end + + def construct_coder(row, coder_class) + return unless coder_class + coder_class.new(oid: row['oid'], name: row['typname']) + end + + ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgresql) + ActiveRecord::Type.add_modifier({ range: true }, OID::Range, adapter: :postgresql) + ActiveRecord::Type.register(:bit, OID::Bit, adapter: :postgresql) + ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql) + ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql) + ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql) + ActiveRecord::Type.register(:date_time, OID::DateTime, adapter: :postgresql) + ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql) + ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql) + ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql) + ActiveRecord::Type.register(:inet, OID::Inet, adapter: :postgresql) + ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql) + ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) + ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) + ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql) + ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql) + ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql) end end end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 4d8afcf16a..37ff4e4613 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,4 +1,3 @@ - module ActiveRecord module ConnectionAdapters class SchemaCache @@ -20,6 +19,7 @@ module ActiveRecord # A cached lookup for table existence. def table_exists?(name) + prepare_tables if @tables.empty? return @tables[name] if @tables.key? name @tables[name] = connection.table_exists?(name) @@ -61,9 +61,7 @@ module ActiveRecord end def size - [@columns, @columns_hash, @primary_keys, @tables].map { |x| - x.size - }.inject :+ + [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+ end # Clear out internal caches for table with +table_name+. @@ -83,6 +81,12 @@ module ActiveRecord def marshal_load(array) @version, @columns, @columns_hash, @primary_keys, @tables = array end + + private + + def prepare_tables + connection.tables.each { |table| @tables[table] = true } + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb new file mode 100644 index 0000000000..ccb7e154ee --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb @@ -0,0 +1,32 @@ +module ActiveRecord + # :stopdoc: + module ConnectionAdapters + class SqlTypeMetadata + attr_reader :sql_type, :type, :limit, :precision, :scale + + def initialize(sql_type: nil, type: nil, limit: nil, precision: nil, scale: nil) + @sql_type = sql_type + @type = type + @limit = limit + @precision = precision + @scale = scale + end + + def ==(other) + other.is_a?(SqlTypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, sql_type, type, limit, precision, scale] + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index e6163771e8..400b586c95 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -14,9 +14,9 @@ module ActiveRecord raise ArgumentError, "No database file specified. Missing argument: database" end - # Allow database path relative to Rails.root, but only if - # the database path is not the special path that tells - # Sqlite to build a database only in memory. + # Allow database path relative to Rails.root, but only if the database + # path is not the special path that tells sqlite to build a database only + # in memory. if ':memory:' != config[:database] config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root) dirname = File.dirname(config[:database]) @@ -41,15 +41,6 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Binary < Type::Binary # :nodoc: - def cast_value(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value - end - end - # The SQLite3 adapter works SQLite 3.6.16 or newer # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). # @@ -57,6 +48,7 @@ module ActiveRecord # # * <tt>:database</tt> - Path to the database file. class SQLite3Adapter < AbstractAdapter + ADAPTER_NAME = 'SQLite'.freeze include Savepoints NATIVE_DATABASE_TYPES = { @@ -77,11 +69,11 @@ module ActiveRecord include Comparable def initialize(version_string) - @version = version_string.split('.').map { |v| v.to_i } + @version = version_string.split('.').map(&:to_i) end def <=>(version_string) - @version <=> version_string.split('.').map { |v| v.to_i } + @version <=> version_string.split('.').map(&:to_i) end end @@ -104,7 +96,7 @@ module ActiveRecord end def clear - cache.values.each do |hash| + cache.each_value do |hash| dealloc hash[:stmt] end cache.clear @@ -137,10 +129,6 @@ module ActiveRecord end end - def adapter_name #:nodoc: - 'SQLite' - end - def supports_ddl_transactions? true end @@ -172,7 +160,7 @@ module ActiveRecord true end - def supports_add_column? + def supports_views? true end @@ -220,13 +208,29 @@ module ActiveRecord # QUOTING ================================================== def _quote(value) # :nodoc: - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data "x'#{value.hex}'" else super end end + def _type_cast(value) # :nodoc: + case value + when BigDecimal + value.to_f + when String + if value.encoding == Encoding::ASCII_8BIT + super(value.encode(Encoding::UTF_8)) + else + super + end + else + super + end + end + def quote_string(s) #:nodoc: @connection.class.quote(s) end @@ -239,30 +243,9 @@ module ActiveRecord %Q("#{name.to_s.gsub('"', '""')}") end - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value - - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger - value = value.encode Encoding::UTF_8 - end - value - end - + #-- # DATABASE STATEMENTS ====================================== + #++ def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" @@ -284,11 +267,9 @@ module ActiveRecord end def exec_query(sql, name = nil, binds = []) - type_casted_binds = binds.map { |col, val| - [col, type_cast(val, col)] - } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - log(sql, name, type_casted_binds) do + log(sql, name, binds) do # Don't cache statements if they are not prepared if without_prepared_statement?(binds) stmt = @connection.prepare(sql) @@ -306,7 +287,7 @@ module ActiveRecord stmt = cache[:stmt] cols = cache[:cols] ||= stmt.columns stmt.reset! - stmt.bind_params type_casted_binds.map { |_, val| val } + stmt.bind_params type_casted_binds end ActiveRecord::Result.new(cols, stmt.to_a) @@ -355,7 +336,7 @@ module ActiveRecord log('commit transaction',nil) { @connection.commit } end - def rollback_db_transaction #:nodoc: + def exec_rollback_db_transaction #:nodoc: log('rollback transaction',nil) { @connection.rollback } end @@ -365,7 +346,7 @@ module ActiveRecord sql = <<-SQL SELECT name FROM sqlite_master - WHERE type = 'table' AND NOT name = 'sqlite_sequence' + WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' SQL sql << " AND name = #{quote_table_name(table_name)}" if table_name @@ -378,7 +359,7 @@ module ActiveRecord table_name && tables(nil, table_name).any? end - # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. + # Returns an array of +Column+ objects for the table specified by +table_name+. def columns(table_name) #:nodoc: table_structure(table_name).map do |field| case field["dflt_value"] @@ -391,8 +372,8 @@ module ActiveRecord end sql_type = field['type'] - cast_type = lookup_cast_type(sql_type) - new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) + type_metadata = fetch_type_metadata(sql_type) + new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0) end end @@ -422,10 +403,9 @@ module ActiveRecord end def primary_key(table_name) #:nodoc: - column = table_structure(table_name).find { |field| - field['pk'] == 1 - } - column && column['name'] + pks = table_structure(table_name).select { |f| f['pk'] > 0 } + return nil unless pks.count == 1 + pks[0]['name'] end def remove_index!(table_name, index_name) #:nodoc: @@ -443,12 +423,12 @@ module ActiveRecord # See: http://www.sqlite.org/lang_altertable.html # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_options( type, options) + def valid_alter_table_type?(type) type.to_sym != :primary_key end def add_column(table_name, column_name, type, options = {}) #:nodoc: - if supports_add_column? && valid_alter_table_options( type, options ) + if valid_alter_table_type?(type) super(table_name, column_name, type, options) else alter_table(table_name) do |definition| @@ -500,15 +480,6 @@ module ActiveRecord protected - def initialize_type_map(m) - super - m.register_type(/binary/i, SQLite3Binary.new) - end - - def select(sql, name = nil, binds = []) #:nodoc: - exec_query(sql, name, binds) - end - def table_structure(table_name) structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? @@ -553,7 +524,7 @@ module ActiveRecord end copy_table_indexes(from, to, options[:rename] || {}) copy_table_contents(from, to, - @definition.columns.map {|column| column.name}, + @definition.columns.map(&:name), options[:rename] || {}) end @@ -566,7 +537,7 @@ module ActiveRecord name = name[1..-1] end - to_column_names = columns(to).map { |c| c.name } + to_column_names = columns(to).map(&:name) columns = index.columns.map {|c| rename[c] || c }.select do |column| to_column_names.include?(column) end @@ -583,25 +554,14 @@ module ActiveRecord def copy_table_contents(from, to, columns, rename = {}) #:nodoc: column_mappings = Hash[columns.map {|name| [name, name]}] rename.each { |a| column_mappings[a.last] = a.first } - from_columns = columns(from).collect {|col| col.name} + from_columns = columns(from).collect(&:name) columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + from_columns_to_copy = columns.map { |col| column_mappings[col] } quoted_columns = columns.map { |col| quote_column_name(col) } * ',' + quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ',' - quoted_to = quote_table_name(to) - - raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }] - - exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| - sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - - column_values = columns.map do |col| - quote(row[column_mappings[col]], raw_column_mappings[col]) - end - - sql << column_values * ', ' - sql << ')' - exec_query sql - end + exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns}) + SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}") end def sqlite_version diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 31e7390bf7..984af79642 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionHandling - RAILS_ENV = -> { Rails.env if defined?(Rails) } + RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" } # Establishes the connection to the database. Accepts a hash as input where diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index d39e5fddfe..44d587206d 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,6 +1,7 @@ +require 'thread' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/object/duplicable' -require 'thread' +require 'active_support/core_ext/string/filters' module ActiveRecord module Core @@ -16,7 +17,6 @@ module ActiveRecord mattr_accessor :logger, instance_writer: false ## - # :singleton-method: # Contains the database configuration - as is typically stored in config/database.yml - # as a Hash. # @@ -85,14 +85,8 @@ module ActiveRecord mattr_accessor :dump_schema_after_migration, instance_writer: false self.dump_schema_after_migration = true - # :nodoc: mattr_accessor :maintain_test_schema, instance_accessor: false - def self.disable_implicit_join_references=(value) - ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \ - "Make sure to remove this configuration because it does nothing.") - end - class_attribute :default_connection_handler, instance_writer: false class_attribute :find_by_statement_cache @@ -108,29 +102,37 @@ module ActiveRecord end module ClassMethods - def initialize_find_by_cache + def allocate + define_attribute_methods + super + end + + def initialize_find_by_cache # :nodoc: self.find_by_statement_cache = {}.extend(Mutex_m) end - def inherited(child_class) + def inherited(child_class) # :nodoc: child_class.initialize_find_by_cache super end - def find(*ids) + def find(*ids) # :nodoc: # We don't have cache keys for this stuff yet return super unless ids.length == 1 return super if block_given? || primary_key.nil? || default_scopes.any? || + current_scope || columns_hash.include?(inheritance_column) || ids.first.kind_of?(Array) id = ids.first if ActiveRecord::Base === id id = id.id - ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \ - "Please pass the id of the object by calling `.id`" + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `find`. + Please pass the id of the object by calling `.id` + MSG end key = primary_key @@ -144,10 +146,13 @@ module ActiveRecord raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" end record + rescue RangeError + raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'" end - def find_by(*args) - return super if current_scope || args.length > 1 || reflect_on_all_aggregations.any? + def find_by(*args) # :nodoc: + return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any? + return super if default_scopes.any? hash = args.first @@ -155,6 +160,9 @@ module ActiveRecord v.nil? || Array === v || Hash === v } + # We can't cache Post.find_by(author: david) ...yet + return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) } + key = hash.keys klass = self @@ -170,12 +178,16 @@ module ActiveRecord s.execute(hash.values, self, connection).first rescue TypeError => e raise ActiveRecord::StatementInvalid.new(e.message, e) + rescue RangeError + nil end end - def initialize_generated_modules - super + def find_by!(*args) # :nodoc: + find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}") + end + def initialize_generated_modules # :nodoc: generated_association_methods end @@ -196,7 +208,7 @@ module ActiveRecord elsif !connected? "#{super} (call '#{super}.connection' to establish a connection)" elsif table_exists? - attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' + attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ', ' "#{super}(#{attr_list})" else "#{super}(Table doesn't exist)" @@ -214,7 +226,7 @@ module ActiveRecord # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) } # end def arel_table # :nodoc: - @arel_table ||= Arel::Table.new(table_name, arel_engine) + @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster) end # Returns the Arel engine. @@ -227,10 +239,18 @@ module ActiveRecord end end + def predicate_builder # :nodoc: + @predicate_builder ||= PredicateBuilder.new(table_metadata) + end + + def type_caster # :nodoc: + TypeCaster::Map.new(self) + end + private - def relation #:nodoc: - relation = Relation.create(self, arel_table) + def relation # :nodoc: + relation = Relation.create(self, arel_table, predicate_builder) if finder_needs_type_condition? relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) @@ -238,6 +258,10 @@ module ActiveRecord relation end end + + def table_metadata # :nodoc: + TableMetadata.new(self, arel_table) + end end # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with @@ -248,26 +272,17 @@ module ActiveRecord # ==== Example: # # Instantiates a single new object # User.new(first_name: 'Jamie') - def initialize(attributes = nil, options = {}) - defaults = {} - self.class.raw_column_defaults.each do |k, v| - default = v.duplicable? ? v.dup : v - defaults[k] = Attribute.from_database(default, type_for_attribute(k)) - end - - @attributes = defaults - @column_types = self.class.column_types + def initialize(attributes = nil) + @attributes = self.class._default_attributes.dup + self.class.define_attribute_methods init_internals initialize_internals_callback - self.class.define_attribute_methods - # +options+ argument is only needed to make protected_attributes gem easier to hook. - # Remove it when we drop support to this gem. - init_attributes(attributes, options) if attributes + assign_attributes(attributes) if attributes yield self if block_given? - run_callbacks :initialize unless _initialize_callbacks.empty? + _run_initialize_callbacks end # Initialize an empty model object from +coder+. +coder+ must contain @@ -282,7 +297,6 @@ module ActiveRecord # post.title # => 'hello world' def init_with(coder) @attributes = coder['attributes'] - @column_types = self.class.column_types init_internals @@ -290,8 +304,8 @@ module ActiveRecord self.class.define_attribute_methods - run_callbacks :find - run_callbacks :initialize + _run_find_callbacks + _run_initialize_callbacks self end @@ -324,11 +338,10 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: - pk = self.class.primary_key - @attributes = other.clone_attributes - @attributes[pk] = Attribute.from_database(nil, type_for_attribute(pk)) + @attributes = @attributes.dup + @attributes.reset(self.class.primary_key) - run_callbacks(:initialize) unless _initialize_callbacks.empty? + _run_initialize_callbacks @aggregation_cache = {} @association_cache = {} @@ -441,6 +454,7 @@ module ActiveRecord # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` # when pp is required. def pretty_print(pp) + return super if custom_inspect_method_defined? pp.object_address_group(self) do if defined?(@attributes) && @attributes column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } @@ -466,16 +480,16 @@ module ActiveRecord Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access end + private + def set_transaction_state(state) # :nodoc: @transaction_state = state end def has_transactional_callbacks? # :nodoc: - !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty? + !_rollback_callbacks.empty? || !_commit_callbacks.empty? end - private - # Updates the attributes on this particular ActiveRecord object so that # if it is associated with a transaction, then the state of the AR object # will be updated to reflect the current state of the transaction @@ -498,6 +512,8 @@ module ActiveRecord end def update_attributes_from_transaction_state(transaction_state, depth) + @reflects_state = [false] if depth == 0 + if transaction_state && transaction_state.finalized? && !has_transactional_callbacks? unless @reflects_state[depth] restore_transaction_record_state if transaction_state.rolledback? @@ -524,9 +540,6 @@ module ActiveRecord end def init_internals - pk = self.class.primary_key - @attributes[pk] ||= Attribute.from_database(nil, type_for_attribute(pk)) - @aggregation_cache = {} @association_cache = {} @readonly = false @@ -537,22 +550,19 @@ module ActiveRecord @txn = nil @_start_transaction_state = {} @transaction_state = nil - @reflects_state = [false] end def initialize_internals_callback end - # This method is needed to make protected_attributes gem easier to hook. - # Remove it when we drop support to this gem. - def init_attributes(attributes, options) - assign_attributes(attributes) - end - def thaw if frozen? @attributes = @attributes.dup end end + + def custom_inspect_method_defined? + self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner + end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 05c4b13016..82596b63df 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -20,7 +20,7 @@ module ActiveRecord def reset_counters(id, *counters) object = find(id) counters.each do |counter_association| - has_many_association = _reflect_on_association(counter_association.to_sym) + has_many_association = _reflect_on_association(counter_association) unless has_many_association has_many = reflect_on_all_associations(:has_many) has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym } @@ -34,13 +34,12 @@ module ActiveRecord foreign_key = has_many_association.foreign_key.to_s child_class = has_many_association.klass - reflection = child_class._reflections.values.find { |e| :belongs_to == e.macro && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } + reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column - stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(counter_association).count - }, primary_key) - connection.update stmt + unscoped.where(primary_key => object.id).update_all( + counter_name => object.send(counter_association).count(:all) + ) end return true end @@ -123,16 +122,6 @@ module ActiveRecord end end - protected - - def actually_destroyed? - @_actually_destroyed - end - - def clear_destroy_state - @_actually_destroyed = nil - end - private def _create_record(*) @@ -167,7 +156,7 @@ module ActiveRecord def each_counter_cached_associations _reflections.each do |name, reflection| - yield association(name) if reflection.belongs_to? && reflection.counter_cache_column + yield association(name.to_sym) if reflection.belongs_to? && reflection.counter_cache_column end end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e94b74063e..b6dd6814db 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,10 +1,5 @@ module ActiveRecord module DynamicMatchers #:nodoc: - # This code in this file seems to have a lot of indirection, but the indirection - # is there to provide extension points for the activerecord-deprecated_finders - # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5), - # then we can remove the indirection. - def respond_to?(name, include_private = false) if self == Base super @@ -72,26 +67,14 @@ module ActiveRecord CODE end - def body - raise NotImplementedError - end - end + private - module Finder - # Extended in activerecord-deprecated_finders def body - result - end - - # Extended in activerecord-deprecated_finders - def result "#{finder}(#{attributes_hash})" end # The parameters in the signature may have reserved Ruby words, in order # to prevent errors, we start each param name with `_`. - # - # Extended in activerecord-deprecated_finders def signature attribute_names.map { |name| "_#{name}" }.join(', ') end @@ -109,7 +92,6 @@ module ActiveRecord class FindBy < Method Method.matchers << self - include Finder def self.prefix "find_by" @@ -122,7 +104,6 @@ module ActiveRecord class FindByBang < Method Method.matchers << self - include Finder def self.prefix "find_by" diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 38c6dcf88d..ea88983917 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -32,6 +32,12 @@ module ActiveRecord # Conversation.active # Conversation.archived # + # Of course, you can also query them directly if the scopes doesn't fit your + # needs: + # + # Conversation.where(status: [:active, :archived]) + # Conversation.where.not(status: :active) + # # You can set the default value from the database declaration, like: # # create_table :conversations do |t| @@ -59,26 +65,60 @@ module ActiveRecord # # In rare circumstances you might need to access the mapping directly. # The mappings are exposed through a class method with the pluralized attribute - # name: + # name, which return the mapping in a +HashWithIndifferentAccess+: # - # Conversation.statuses # => { "active" => 0, "archived" => 1 } + # Conversation.statuses[:active] # => 0 + # Conversation.statuses["archived"] # => 1 # - # Use that class method when you need to know the ordinal value of an enum: + # Use that class method when you need to know the ordinal value of an enum. + # For example, you can use that when manually building SQL strings: # # Conversation.where("status <> ?", Conversation.statuses[:archived]) # - # Where conditions on an enum attribute must use the ordinal value of an enum. + module Enum - def self.extended(base) + def self.extended(base) # :nodoc: base.class_attribute(:defined_enums) base.defined_enums = {} end - def inherited(base) + def inherited(base) # :nodoc: base.defined_enums = defined_enums.deep_dup super end + class EnumType < Type::Value + def initialize(name, mapping) + @name = name + @mapping = mapping + end + + def cast(value) + return if value.blank? + + if mapping.has_key?(value) + value.to_s + elsif mapping.has_value?(value) + mapping.key(value) + else + raise ArgumentError, "'#{value}' is not a valid #{name}" + end + end + + def deserialize(value) + return if value.nil? + mapping.key(value.to_i) + end + + def serialize(value) + mapping.fetch(value, value) + end + + protected + + attr_reader :name, :mapping + end + def enum(definitions) klass = self definitions.each do |name, values| @@ -90,37 +130,19 @@ module ActiveRecord detect_enum_conflict!(name, name.to_s.pluralize, true) klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } - _enum_methods_module.module_eval do - # def status=(value) self[:status] = statuses[value] end - klass.send(:detect_enum_conflict!, name, "#{name}=") - define_method("#{name}=") { |value| - if enum_values.has_key?(value) || value.blank? - self[name] = enum_values[value] - elsif enum_values.has_value?(value) - # Assigning a value directly is not a end-user feature, hence it's not documented. - # This is used internally to make building objects from the generated scopes work - # as expected, i.e. +Conversation.archived.build.archived?+ should be true. - self[name] = value - else - raise ArgumentError, "'#{value}' is not a valid #{name}" - end - } - - # def status() statuses.key self[:status] end - klass.send(:detect_enum_conflict!, name, name) - define_method(name) { enum_values.key self[name] } + detect_enum_conflict!(name, name) + detect_enum_conflict!(name, "#{name}=") - # def status_before_type_cast() statuses.key self[:status] end - klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast") - define_method("#{name}_before_type_cast") { enum_values.key self[name] } + attribute name, EnumType.new(name, enum_values) + _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index pairs.each do |value, i| enum_values[value] = i # def active?() status == 0 end klass.send(:detect_enum_conflict!, name, "#{value}?") - define_method("#{value}?") { self[name] == i } + define_method("#{value}?") { self[name] == value.to_s } # def active!() update! status: :active end klass.send(:detect_enum_conflict!, name, "#{value}!") @@ -128,7 +150,7 @@ module ActiveRecord # scope :active, -> { where status: 0 } klass.send(:detect_enum_conflict!, name, value, true) - klass.scope value, -> { klass.where name => i } + klass.scope value, -> { klass.where name => value } end end defined_enums[name.to_s] = enum_values @@ -138,25 +160,7 @@ module ActiveRecord private def _enum_methods_module @_enum_methods_module ||= begin - mod = Module.new do - private - def save_changed_attribute(attr_name, old) - if (mapping = self.class.defined_enums[attr_name.to_s]) - value = read_attribute(attr_name) - if attribute_changed?(attr_name) - if mapping[old] == value - changed_attributes.delete(attr_name) - end - else - if old != value - changed_attributes[attr_name] = mapping.key old - end - end - else - super - end - end - end + mod = Module.new include mod mod end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 52c70977ef..d710d96a9a 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -52,10 +52,29 @@ module ActiveRecord # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be # saved because record is invalid. class RecordNotSaved < ActiveRecordError + attr_reader :record + + def initialize(message, record = nil) + @record = record + super(message) + end end # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false. + # + # begin + # complex_operation_that_internally_calls_destroy! + # rescue ActiveRecord::RecordNotDestroyed => invalid + # puts invalid.record.errors + # end + # class RecordNotDestroyed < ActiveRecordError + attr_reader :record + + def initialize(record) + @record = record + super() + end end # Superclass for all database execution errors. @@ -159,24 +178,17 @@ module ActiveRecord class DangerousAttributeError < ActiveRecordError end - # Raised when unknown attributes are supplied via mass assignment. - class UnknownAttributeError < NoMethodError - - attr_reader :record, :attribute - - def initialize(record, attribute) - @record = record - @attribute = attribute.to_s - super("unknown attribute: #{attribute}") - end - - end + UnknownAttributeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new( # :nodoc: + 'ActiveRecord::UnknownAttributeError', + 'ActiveModel::AttributeAssignment::UnknownAttributeError' + ) # Raised when an error occurred while doing a mass assignment to an attribute through the # +attributes=+ method. The exception has an +attribute+ property that is the name of the # offending attribute. class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute + def initialize(message, exception, attribute) super(message) @exception = exception @@ -189,6 +201,7 @@ module ActiveRecord # objects, each corresponding to the error while assigning to an attribute. class MultiparameterAssignmentErrors < ActiveRecordError attr_reader :errors + def initialize(errors) @errors = errors end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index e65dab07ba..727a9befc1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -27,7 +27,7 @@ module ActiveRecord end.join("\n") end.join("\n") - # Overriding inspect to be more human readable, specially in the console. + # Overriding inspect to be more human readable, especially in the console. def str.inspect self end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 4cd5f92207..739be524da 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -1,8 +1,9 @@ require 'erb' require 'yaml' require 'zlib' +require 'set' require 'active_support/dependencies' -require 'active_support/core_ext/securerandom' +require 'active_support/core_ext/digest/uuid' require 'active_record/fixture_set/file' require 'active_record/errors' @@ -126,12 +127,12 @@ module ActiveRecord # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>. # # - define a helper method in `test_helper.rb` - # class FixtureFileHelpers + # module FixtureFileHelpers # def file_sha(path) # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path))) # end # end - # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers + # ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers # # - use the helper method in a fixture # photo: @@ -181,6 +182,9 @@ module ActiveRecord # * Stable, autogenerated IDs # * Label references for associations (belongs_to, has_one, has_many) # * HABTM associations as inline lists + # + # There are some more advanced features available even if the id is specified: + # # * Autofilled timestamp columns # * Fixture label interpolation # * Support for YAML defaults @@ -515,15 +519,19 @@ module ActiveRecord ::File.join(fixtures_directory, fs_name)) end - all_loaded_fixtures.update(fixtures_map) + update_all_loaded_fixtures fixtures_map connection.transaction(:requires_new => true) do + deleted_tables = Set.new fixture_sets.each do |fs| conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection table_rows = fs.table_rows - table_rows.keys.each do |table| - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + table_rows.each_key do |table| + unless deleted_tables.include? table + conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + end + deleted_tables << table end table_rows.each do |fixture_set_name, rows| @@ -551,7 +559,7 @@ module ActiveRecord # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. def self.identify(label, column_type = :integer) if column_type == :uuid - SecureRandom.uuid_v5(SecureRandom::UUID_OID_NAMESPACE, label.to_s) + Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) else Zlib.crc32(label.to_s) % MAX_ID end @@ -562,6 +570,10 @@ module ActiveRecord @context_class ||= Class.new end + def self.update_all_loaded_fixtures(fixtures_map) # :nodoc: + all_loaded_fixtures.update(fixtures_map) + end + attr_reader :table_name, :name, :fixtures, :model_class, :config def initialize(connection, name, class_name, path, config = ActiveRecord::Base) @@ -626,7 +638,7 @@ module ActiveRecord # interpolate the fixture label row.each do |key, value| - row[key] = value.gsub("$LABEL", label) if value.is_a?(String) + row[key] = value.gsub("$LABEL", label.to_s) if value.is_a?(String) end # generate a primary key if necessary @@ -642,7 +654,7 @@ module ActiveRecord model_class end - reflection_class._reflections.values.each do |association| + reflection_class._reflections.each_value do |association| case association.macro when :belongs_to # Do not replace association name with association foreign key if they are named the same @@ -654,7 +666,7 @@ module ActiveRecord row[association.foreign_type] = $1 end - fk_type = association.active_record.columns_hash[association.foreign_key].type + fk_type = association.active_record.type_for_attribute(fk_name).type row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) end when :has_many @@ -684,7 +696,7 @@ module ActiveRecord end def primary_key_type - @association.klass.column_types[@association.klass.primary_key].type + @association.klass.type_for_attribute(@association.klass.primary_key).type end end @@ -704,7 +716,7 @@ module ActiveRecord end def primary_key_type - @primary_key_type ||= model_class && model_class.column_types[model_class.primary_key].type + @primary_key_type ||= model_class && model_class.type_for_attribute(model_class.primary_key).type end def add_join_records(rows, row, association) @@ -738,7 +750,7 @@ module ActiveRecord end def column_names - @column_names ||= @connection.columns(@table_name).collect { |c| c.name } + @column_names ||= @connection.columns(@table_name).collect(&:name) end def read_fixture_files(path, model_class) @@ -761,12 +773,6 @@ module ActiveRecord end - #-- - # Deprecate 'Fixtures' in favor of 'FixtureSet'. - #++ - # :nodoc: - Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', 'ActiveRecord::FixtureSet') - class Fixture #:nodoc: include Enumerable @@ -799,7 +805,9 @@ module ActiveRecord def find if model_class - model_class.find(fixture[model_class.primary_key]) + model_class.unscoped do + model_class.find(fixture[model_class.primary_key]) + end else raise FixtureClassNotFound, "No class attached to find." end @@ -859,38 +867,13 @@ module ActiveRecord fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"] fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] } else - fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } + fixture_set_names = fixture_set_names.flatten.map(&:to_s) end self.fixture_table_names |= fixture_set_names - require_fixture_classes(fixture_set_names, self.config) setup_fixture_accessors(fixture_set_names) end - def try_to_load_dependency(file_name) - require_dependency file_name - rescue LoadError => e - # 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 - ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}") - end - end - - def require_fixture_classes(fixture_set_names = nil, config = ActiveRecord::Base) - if fixture_set_names - fixture_set_names = fixture_set_names.map { |n| n.to_s } - else - fixture_set_names = fixture_table_names - end - - fixture_set_names.each do |file_name| - file_name = file_name.singularize if config.pluralize_table_names - try_to_load_dependency(file_name) - end - end - def setup_fixture_accessors(fixture_set_names = nil) fixture_set_names = Array(fixture_set_names || fixture_table_names) methods = Module.new do @@ -904,7 +887,7 @@ module ActiveRecord @fixture_cache[fs_name] ||= {} instances = fixture_names.map do |f_name| - f_name = f_name.to_s + f_name = f_name.to_s if f_name.is_a?(Symbol) @fixture_cache[fs_name].delete(f_name) if force_reload if @loaded_fixtures[fs_name][f_name] @@ -924,7 +907,7 @@ module ActiveRecord def uses_transaction(*methods) @uses_transaction = [] unless defined?(@uses_transaction) - @uses_transaction.concat methods.map { |m| m.to_s } + @uses_transaction.concat methods.map(&:to_s) end def uses_transaction?(method) @@ -967,7 +950,7 @@ module ActiveRecord end # Instantiate fixtures for every test if requested. - instantiate_fixtures(config) if use_instantiated_fixtures + instantiate_fixtures if use_instantiated_fixtures end def teardown_fixtures @@ -994,16 +977,9 @@ module ActiveRecord Hash[fixtures.map { |f| [f.name, f] }] end - # for pre_loaded_fixtures, only require the classes once. huge speed improvement - @@required_fixture_classes = false - - def instantiate_fixtures(config) + def instantiate_fixtures if pre_loaded_fixtures raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty? - unless @@required_fixture_classes - self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys, config - @@required_fixture_classes = true - end ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?) else raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 4a7aace460..a388b529c9 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -1,12 +1,12 @@ module ActiveRecord - # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt> + # Returns the version of the currently loaded Active Record as a <tt>Gem::Version</tt> def self.gem_version Gem::Version.new VERSION::STRING end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 08fc91c9df..24098f72dc 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,6 +1,37 @@ require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord + # == Single table inheritance + # + # Active Record allows inheritance by storing the name of the class in a column that by + # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). + # This means that an inheritance looking like this: + # + # class Company < ActiveRecord::Base; end + # class Firm < Company; end + # class Client < Company; end + # class PriorityClient < Client; end + # + # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in + # the companies table with type = "Firm". You can then fetch this row again using + # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. + # + # Be aware that because the type column is an attribute on the record every new + # subclass will instantly be marked as dirty and the type column will be included + # in the list of changed attributes on the record. This is different from non + # STI classes: + # + # Company.new.changed? # => false + # Firm.new.changed? # => true + # Firm.new.changes # => {"type"=>["","Firm"]} + # + # If you don't have a type column defined in your table, single-table inheritance won't + # be triggered. In that case, it'll work just like normal subclasses with no special magic + # for differentiating between them or reloading the right type with find. + # + # Note, all the attributes for all the cases are kept in the same table. Read more: + # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # module Inheritance extend ActiveSupport::Concern @@ -48,16 +79,6 @@ module ActiveRecord :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) end - def symbolized_base_class - ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_base_class is deprecated and will be removed without replacement.") - @symbolized_base_class ||= base_class.to_s.to_sym - end - - def symbolized_sti_name - ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_sti_name is deprecated and will be removed without replacement.") - @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class - end - # Returns the class descending directly from ActiveRecord::Base, or # an abstract class, if any, in the inheritance hierarchy. # @@ -120,14 +141,8 @@ module ActiveRecord candidates << type_name candidates.each do |candidate| - begin - constant = ActiveSupport::Dependencies.constantize(candidate) - return constant if candidate == constant.to_s - # We don't want to swallow NoMethodError < NameError errors - rescue NoMethodError - raise - rescue NameError - end + constant = ActiveSupport::Dependencies.safe_constantize(candidate) + return constant if candidate == constant.to_s end raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) @@ -167,7 +182,7 @@ module ActiveRecord def type_condition(table = arel_table) sti_column = table[inheritance_column] - sti_names = ([self] + descendants).map { |model| model.sti_name } + sti_names = ([self] + descendants).map(&:sti_name) sti_column.in(sti_names) end @@ -177,7 +192,7 @@ module ActiveRecord # If this is a StrongParameters hash, and access to inheritance_column is not permitted, # this will ignore the inheritance column and return nil def subclass_from_attributes?(attrs) - columns_hash.include?(inheritance_column) && attrs.is_a?(Hash) + attribute_names.include?(inheritance_column) && attrs.is_a?(Hash) end def subclass_from_attributes(attrs) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 31e2518540..15b2f65dcb 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -55,16 +55,16 @@ module ActiveRecord def cache_key(*timestamp_names) case when new_record? - "#{self.class.model_name.cache_key}/new" + "#{model_name.cache_key}/new" when timestamp_names.any? timestamp = max_updated_column_timestamp(timestamp_names) timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" + "#{model_name.cache_key}/#{id}-#{timestamp}" when timestamp = max_updated_column_timestamp timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" + "#{model_name.cache_key}/#{id}-#{timestamp}" else - "#{self.class.model_name.cache_key}/#{id}" + "#{model_name.cache_key}/#{id}" end end diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index b1fbd38622..8a3c27e6da 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -7,6 +7,7 @@ en: # Default error messages errors: messages: + required: "must exist" taken: "has already been taken" # Active Record models configuration diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 4528d8783c..a58d8355aa 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -11,7 +11,7 @@ module ActiveRecord # # == Usage # - # Active Records support optimistic locking if the field +lock_version+ is present. Each update to the + # Active Record supports optimistic locking if the +lock_version+ field is present. Each update to the # record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice # will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example: # @@ -66,6 +66,15 @@ module ActiveRecord send(lock_col + '=', previous_lock_value + 1) end + def _create_record(attribute_names = self.attribute_names, *) # :nodoc: + if locking_enabled? + # We always want to persist the locking version, even if we don't detect + # a change from the default, since the database might have no default + attribute_names |= [self.class.locking_column] + end + super + end + def _update_record(attribute_names = self.attribute_names) #:nodoc: return super unless locking_enabled? return 0 if attribute_names.empty? @@ -80,17 +89,15 @@ module ActiveRecord begin relation = self.class.unscoped - stmt = relation.where( - relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) - ) - ).arel.compile_update( - arel_attributes_with_values_for_update(attribute_names), - self.class.primary_key + affected_rows = relation.where( + self.class.primary_key => id, + lock_col => previous_lock_value, + ).update_all( + attribute_names.map do |name| + [name, _read_attribute(name)] + end.to_h ) - affected_rows = self.class.connection.update stmt - unless affected_rows == 1 raise ActiveRecord::StaleObjectError.new(self, "update") end @@ -118,12 +125,8 @@ module ActiveRecord relation = super if locking_enabled? - column_name = self.class.locking_column - column = self.class.columns_hash[column_name] - substitute = self.class.connection.substitute_at(column, relation.bind_values.length) - - relation = relation.where(self.class.arel_table[column_name].eq(substitute)) - relation.bind_values << [column, self[column_name].to_i] + locking_column = self.class.locking_column + relation = relation.where(locking_column => _read_attribute(locking_column)) end relation @@ -141,7 +144,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - @column_defaults = nil + reload_schema_from_cache @locking_column = value.to_s end @@ -163,18 +166,37 @@ module ActiveRecord super end - def column_defaults - @column_defaults ||= begin - defaults = super - - if defaults.key?(locking_column) && lock_optimistically - defaults[locking_column] ||= 0 + private + + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `lock_optimistically`, or + # `locking_column` would not be picked up. + def inherited(subclass) + subclass.class_eval do + is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } + decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type| + LockingType.new(type) end - - defaults end + super end end end + + class LockingType < DelegateClass(Type::Value) # :nodoc: + def deserialize(value) + # `nil` *should* be changed to 0 + super.to_i + end + + def init_with(coder) + __setobj__(coder['subtype']) + end + + def encode_with(coder) + coder['subtype'] = __getobj__ + end + end end end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index eb64d197f0..6b26d7be78 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -20,18 +20,14 @@ module ActiveRecord @odd = false end - def render_bind(column, value) - if column - if column.binary? - # This specifically deals with the PG adapter that casts bytea columns into a Hash. - value = value[:value] if value.is_a?(Hash) - value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>" - end - - [column.name, value] + def render_bind(attribute) + value = if attribute.type.binary? && attribute.value + "<#{attribute.value.bytesize} bytes of binary data>" else - [nil, value] + attribute.value_for_database end + + [attribute.name, value] end def sql(event) @@ -47,9 +43,7 @@ module ActiveRecord binds = nil unless (payload[:binds] || []).empty? - binds = " " + payload[:binds].map { |col,v| - render_bind(col, v) - }.inspect + binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect end if odd? diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 481e5c17e4..46f4794010 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -39,7 +39,7 @@ module ActiveRecord class PendingMigrationError < MigrationError#:nodoc: def initialize - if defined?(Rails) + if defined?(Rails.env) super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}") else super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate") @@ -161,21 +161,14 @@ module ActiveRecord # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the # UTC formatted date and time that the migration was generated. # - # You may then edit the <tt>up</tt> and <tt>down</tt> methods of - # MyNewMigration. - # # There is a special syntactic shortcut to generate migrations that add fields to a table. # # rails generate migration add_fieldname_to_tablename fieldname:string # # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: # class AddFieldnameToTablename < ActiveRecord::Migration - # def up - # add_column :tablenames, :fieldname, :string - # end - # - # def down - # remove_column :tablenames, :fieldname + # def change + # add_column :tablenames, :field, :string # end # end # @@ -188,9 +181,12 @@ module ActiveRecord # # To roll the database back to a previous migration version, use # <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which - # you wish to downgrade. If any of the migrations throw an - # <tt>ActiveRecord::IrreversibleMigration</tt> exception, that step will fail and you'll - # have some manual work to do. + # you wish to downgrade. Alternatively, you can also use the STEP option if you + # wish to rollback last few migrations. <tt>rake db:migrate STEP=2</tt> will rollback + # the latest two migrations. + # + # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception, + # that step will fail and you'll have some manual work to do. # # == Database support # @@ -372,13 +368,21 @@ module ActiveRecord end def call(env) - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i - if @last_check < mtime - ActiveRecord::Migration.check_pending! - @last_check = mtime + if connection.supports_migrations? + mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + if @last_check < mtime + ActiveRecord::Migration.check_pending!(connection) + @last_check = mtime + end end @app.call(env) end + + private + + def connection + ActiveRecord::Base.connection + end end class << self @@ -390,8 +394,15 @@ module ActiveRecord end def load_schema_if_pending! - if ActiveRecord::Migrator.needs_migration? - ActiveRecord::Tasks::DatabaseTasks.load_schema + if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? + # Roundrip to Rake to allow plugins to hook into database initialization. + FileUtils.cd Rails.root do + current_config = Base.connection_config + Base.clear_all_connections! + system("bin/rake db:test:prepare") + # Establish a new connection, the old database may be gone (db:test:prepare uses purge) + Base.establish_connection(current_config) + end check_pending! end end @@ -636,13 +647,15 @@ module ActiveRecord end def method_missing(method, *arguments, &block) - arg_list = arguments.map{ |a| a.inspect } * ', ' + arg_list = arguments.map(&:inspect) * ', ' say_with_time "#{method}(#{arg_list})" do unless @connection.respond_to? :revert unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) - arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table + if [:rename_table, :add_foreign_key].include?(method) + arguments[1] = proper_table_name(arguments.second, table_name_options) + end end end return super unless connection.respond_to?(method) @@ -804,43 +817,46 @@ module ActiveRecord migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? - self.new(:up, migrations, target_version).migrate + new(:up, migrations, target_version).migrate end def down(migrations_paths, target_version = nil, &block) migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? - self.new(:down, migrations, target_version).migrate + new(:down, migrations, target_version).migrate end def run(direction, migrations_paths, target_version) - self.new(direction, migrations(migrations_paths), target_version).run + new(direction, migrations(migrations_paths), target_version).run end def open(migrations_paths) - self.new(:up, migrations(migrations_paths), nil) + new(:up, migrations(migrations_paths), nil) end def schema_migrations_table_name SchemaMigration.table_name end - def get_all_versions - SchemaMigration.all.map { |x| x.version.to_i }.sort + def get_all_versions(connection = Base.connection) + if connection.table_exists?(schema_migrations_table_name) + SchemaMigration.all.map { |x| x.version.to_i }.sort + else + [] + end end def current_version(connection = Base.connection) - sm_table = schema_migrations_table_name - if connection.table_exists?(sm_table) - get_all_versions.max || 0 - else - 0 - end + get_all_versions(connection).max || 0 end def needs_migration?(connection = Base.connection) - current_version(connection) < last_version + (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0 + end + + def any_migrations? + migrations(migrations_paths).any? end def last_version @@ -882,7 +898,7 @@ module ActiveRecord private def move(direction, migrations_paths, steps) - migrator = self.new(direction, migrations(migrations_paths)) + migrator = new(direction, migrations(migrations_paths)) start_index = migrator.migrations.index(migrator.current_migration) if start_index diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index c44d8c1665..36256415df 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -74,7 +74,9 @@ module ActiveRecord :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column_default, :add_reference, :remove_reference, :transaction, :drop_join_table, :drop_table, :execute_block, :enable_extension, - :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too + :change_column, :execute, :remove_columns, :change_column_null, + :add_foreign_key, :remove_foreign_key + # irreversible methods need to be here too ].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) @@ -85,7 +87,7 @@ module ActiveRecord alias :add_belongs_to :add_reference alias :remove_belongs_to :remove_reference - def change_table(table_name, options = {}) + def change_table(table_name, options = {}) # :nodoc: yield delegate.update_table_definition(table_name, self) end @@ -167,6 +169,21 @@ module ActiveRecord [:change_column_null, args] end + def invert_add_foreign_key(args) + from_table, to_table, add_options = args + add_options ||= {} + + if add_options[:name] + options = { name: add_options[:name] } + elsif add_options[:column] + options = { column: add_options[:column] } + else + options = to_table + end + + [:remove_foreign_key, [from_table, options]] + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) if @delegate.respond_to?(method) diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb index ebf64cbcdc..05569fadbd 100644 --- a/activerecord/lib/active_record/migration/join_table.rb +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -8,7 +8,7 @@ module ActiveRecord end def join_table_name(table_1, table_2) - [table_1.to_s, table_2.to_s].sort.join("_").to_sym + ModelSchema.derive_join_table_name(table_1, table_2).to_sym end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 9e1afd32e6..75adcccce6 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -55,6 +55,17 @@ module ActiveRecord delegate :type_for_attribute, to: :class end + # Derives the join table name for +first_table+ and +second_table+. The + # table names appear in alphabetical order. A common prefix is removed + # (useful for namespaced models like Music::Artist and Music::Record): + # + # artists, records => artists_records + # records, artists => artists_records + # music_artists, music_records => music_artists_records + def self.derive_join_table_name(first_table, second_table) # :nodoc: + [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") + end + module ClassMethods # Guesses the table name (in forced lower-case) based on the name of the class in the # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy @@ -100,17 +111,6 @@ module ActiveRecord # class Mouse < ActiveRecord::Base # self.table_name = "mice" # end - # - # Alternatively, you can override the table_name method to define your - # own computation. (Possibly using <tt>super</tt> to manipulate the default - # table name.) Example: - # - # class Post < ActiveRecord::Base - # def self.table_name - # "special_" + super - # end - # end - # Post.table_name # => "special_posts" def table_name reset_table_name unless defined?(@table_name) @table_name @@ -121,9 +121,6 @@ module ActiveRecord # class Project < ActiveRecord::Base # self.table_name = "project" # end - # - # You can also just define your own <tt>self.table_name</tt> method; see - # the documentation for ActiveRecord::Base#table_name. def table_name=(value) value = value && value.to_s @@ -136,7 +133,7 @@ module ActiveRecord @quoted_table_name = nil @arel_table = nil @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name - @relation = Relation.create(self, arel_table) + @predicate_builder = nil end # Returns a quoted version of the table name, used to construct SQL statements. @@ -219,45 +216,43 @@ module ActiveRecord connection.schema_cache.table_exists?(table_name) end - def column_types # :nodoc: - @column_types ||= decorate_types(build_types_hash) + def attributes_builder # :nodoc: + @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) end - def type_for_attribute(attr_name) # :nodoc: - column_types.fetch(attr_name) { Type::Value.new } + def columns_hash # :nodoc: + load_schema + @columns_hash end - def decorate_types(types) # :nodoc: - return if types.empty? - - @time_zone_column_names ||= self.columns_hash.find_all do |name, col| - create_time_zone_conversion_attribute?(name, col) - end.map!(&:first) + def columns + load_schema + @columns ||= columns_hash.values + end - @time_zone_column_names.each do |name| - types[name] = AttributeMethods::TimeZoneConversion::Type.new(types[name]) - end + def attribute_types # :nodoc: + load_schema + @attribute_types ||= Hash.new(Type::Value.new) + end - types + def type_for_attribute(attr_name) # :nodoc: + attribute_types[attr_name] end # Returns a hash where the keys are column names and the values are # default values when instantiating the AR object for this table. def column_defaults - @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }] + load_schema + _default_attributes.to_hash end - # Returns a hash where the keys are the column names and the values - # are the default values suitable for use in `@raw_attriubtes` - def raw_column_defaults # :nodoc: - @raw_column_defauts ||= Hash[column_defaults.map { |name, default| - [name, columns_hash[name].type_cast_for_database(default)] - }] + def _default_attributes # :nodoc: + @default_attributes ||= AttributeSet.new({}) end # Returns an array of column names as strings. def column_names - @column_names ||= columns.map { |column| column.name } + @column_names ||= columns.map(&:name) end # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", @@ -295,23 +290,53 @@ module ActiveRecord def reset_column_information connection.clear_cache! undefine_attribute_methods - connection.schema_cache.clear_table_cache!(table_name) if table_exists? - - @arel_engine = nil - @column_defaults = nil - @raw_column_defauts = nil - @column_names = nil - @column_types = nil - @content_columns = nil - @dynamic_methods_hash = nil - @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column - @relation = nil - @time_zone_column_names = nil - @cached_time_zone = nil + connection.schema_cache.clear_table_cache!(table_name) + + reload_schema_from_cache end private + def schema_loaded? + defined?(@columns_hash) && @columns_hash + end + + def load_schema + unless schema_loaded? + load_schema! + end + end + + def load_schema! + @columns_hash = connection.schema_cache.columns_hash(table_name) + @columns_hash.each do |name, column| + define_attribute( + name, + connection.lookup_cast_type_from_column(column), + default: column.default, + user_provided_default: false + ) + end + end + + def reload_schema_from_cache + @arel_engine = nil + @arel_table = nil + @column_names = nil + @attribute_types = nil + @content_columns = nil + @default_attributes = nil + @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column + @attributes_builder = nil + @column_names = nil + @attribute_types = nil + @columns = nil + @columns_hash = nil + @content_columns = nil + @default_attributes = nil + @attribute_names = nil + end + # Guesses the table name, but does not decorate it with prefix and suffix information. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore @@ -335,10 +360,6 @@ module ActiveRecord base.table_name end end - - def build_types_hash - Hash[columns.map { |column| [column.name, column.cast_type] }] - end end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 8a2a06f2ca..084ef397a8 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -81,6 +81,9 @@ module ActiveRecord # # Note that the model will _not_ be destroyed until the parent is saved. # + # Also note that the model will not be destroyed unless you also specify + # its id in the updated hash. + # # === One-to-many # # Consider a member that has a number of posts: @@ -111,7 +114,7 @@ module ActiveRecord # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' # - # You may also set a :reject_if proc to silently ignore any new record + # You may also set a +:reject_if+ proc to silently ignore any new record # hashes if they fail to pass your criteria. For example, the previous # example could be rewritten as: # @@ -133,7 +136,7 @@ module ActiveRecord # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' # - # Alternatively, :reject_if also accepts a symbol for using methods: + # Alternatively, +:reject_if+ also accepts a symbol for using methods: # # class Member < ActiveRecord::Base # has_many :posts @@ -212,13 +215,13 @@ module ActiveRecord # All changes to models, including the destruction of those marked for # destruction, are saved and destroyed automatically and atomically when # the parent model is saved. This happens inside the transaction initiated - # by the parents save method. See ActiveRecord::AutosaveAssociation. + # by the parent's save method. See ActiveRecord::AutosaveAssociation. # # === Validating the presence of a parent model # # If you want to validate that a child record is associated with a parent - # record, you can use <tt>validates_presence_of</tt> and - # <tt>inverse_of</tt> as this example illustrates: + # record, you can use the +validates_presence_of+ method and the +:inverse_of+ + # key as this example illustrates: # # class Member < ActiveRecord::Base # has_many :posts, inverse_of: :member @@ -230,7 +233,7 @@ module ActiveRecord # validates_presence_of :member # end # - # Note that if you do not specify the <tt>inverse_of</tt> option, then + # Note that if you do not specify the +:inverse_of+ option, then # Active Record will try to automatically guess the inverse association # based on heuristics. # @@ -264,29 +267,31 @@ module ActiveRecord # Allows you to specify a Proc or a Symbol pointing to a method # that checks whether a record should be built for a certain attribute # hash. The hash is passed to the supplied Proc or the method - # and it should return either +true+ or +false+. When no :reject_if + # and it should return either +true+ or +false+. When no +:reject_if+ # is specified, a record will be built for all attribute hashes that # do not have a <tt>_destroy</tt> value that evaluates to true. # Passing <tt>:all_blank</tt> instead of a Proc will create a proc # that will reject a record where all the attributes are blank excluding - # any value for _destroy. + # any value for +_destroy+. # [:limit] - # Allows you to specify the maximum number of the associated records that - # can be processed with the nested attributes. Limit also can be specified as a - # Proc or a Symbol pointing to a method that should return number. If the size of the - # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords - # exception is raised. If omitted, any number associations can be processed. - # Note that the :limit option is only applicable to one-to-many associations. + # Allows you to specify the maximum number of associated records that + # can be processed with the nested attributes. Limit also can be specified + # as a Proc or a Symbol pointing to a method that should return a number. + # If the size of the nested attributes array exceeds the specified limit, + # NestedAttributes::TooManyRecords exception is raised. If omitted, any + # number of associations can be processed. + # Note that the +:limit+ option is only applicable to one-to-many + # associations. # [:update_only] # For a one-to-one association, this option allows you to specify how - # nested attributes are to be used when an associated record already + # nested attributes are going to be used when an associated record already # exists. In general, an existing record may either be updated with the # new set of attribute values or be replaced by a wholly new record - # containing those values. By default the :update_only option is +false+ + # containing those values. By default the +:update_only+ option is +false+ # and the nested attributes are used to update the existing record only # if they include the record's <tt>:id</tt> value. Otherwise a new # record will be instantiated and used to replace the existing one. - # However if the :update_only option is +true+, the nested attributes + # However if the +:update_only+ option is +true+, the nested attributes # are used to update the record's attributes always, regardless of # whether the <tt>:id</tt> is present. The option is ignored for collection # associations. @@ -307,7 +312,7 @@ module ActiveRecord attr_names.each do |association_name| if reflection = _reflect_on_association(association_name) reflection.autosave = true - add_autosave_association_callbacks(reflection) + define_autosave_validation_callbacks(reflection) nested_attributes_options = self.nested_attributes_options.dup nested_attributes_options[association_name.to_sym] = options @@ -516,7 +521,7 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - Type::Boolean.new.type_cast_from_user(hash['_destroy']) + Type::Boolean.new.cast(hash['_destroy']) end # Determines if a new record should be rejected by checking diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb index dbf4564ae5..edb5066fa0 100644 --- a/activerecord/lib/active_record/no_touching.rb +++ b/activerecord/lib/active_record/no_touching.rb @@ -45,7 +45,7 @@ module ActiveRecord NoTouching.applied_to?(self.class) end - def touch(*) + def touch(*) # :nodoc: super unless no_touching? end end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 807c301596..fa6eaa7e64 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -30,10 +30,18 @@ module ActiveRecord true end + def none? + true + end + def any? false end + def one? + false + end + def many? false end @@ -62,9 +70,7 @@ module ActiveRecord calculate :maximum, nil end - def calculate(operation, _column_name, _options = {}) - # TODO: Remove _options argument as soon we remove support to - # activerecord-deprecated_finders. + def calculate(operation, _column_name) if [:count, :sum, :size].include? operation group_values.any? ? Hash.new : 0 elsif [:average, :minimum, :maximum].include?(operation) && group_values.any? @@ -77,5 +83,9 @@ module ActiveRecord def exists?(_id = false) false end + + def or(other) + other.spawn + end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 5c744762d9..af7aef6e43 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -36,6 +36,23 @@ module ActiveRecord end end + # Creates an object (or multiple objects) and saves it to the database, + # if validations pass. Raises a RecordInvalid error if validations fail, + # unlike Base#create. + # + # The +attributes+ parameter can be either a Hash or an Array of Hashes. + # These describe which attributes to be created on the object, or + # multiple objects when given an Array of Hashes. + def create!(attributes = nil, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create!(attr, &block) } + else + object = new(attributes, &block) + object.save! + object + end + end + # Given an attributes hash, +instantiate+ returns a new instance of # the appropriate class. Accepts only keys as strings. # @@ -48,11 +65,7 @@ module ActiveRecord # how this "single-table" inheritance mapping is implemented. def instantiate(attributes, column_types = {}) klass = discriminate_class_for_record(attributes) - - attributes = attributes.each_with_object({}) do |(name, value), h| - type = column_types.fetch(name) { klass.type_for_attribute(name) } - h[name] = Attribute.from_database(value, type) - end + attributes = klass.attributes_builder.build_from_database(attributes, column_types) klass.allocate.init_with('attributes' => attributes, 'new_record' => false) end @@ -96,37 +109,45 @@ module ActiveRecord # validate: false, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # - # There's a series of callbacks associated with +save+. If any of the - # <tt>before_*</tt> callbacks return +false+ the action is cancelled and - # +save+ returns +false+. See ActiveRecord::Callbacks for further + # By default, #save also sets the +updated_at+/+updated_on+ attributes to + # the current time. However, if you supply <tt>touch: false</tt>, these + # timestamps will not be updated. + # + # There's a series of callbacks associated with #save. If any of the + # <tt>before_*</tt> callbacks throws +:abort+ the action is cancelled and + # #save returns +false+. See ActiveRecord::Callbacks for further # details. # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save(*) - create_or_update + def save(*args) + create_or_update(*args) rescue ActiveRecord::RecordInvalid false end # Saves the model. # - # If the model is new a record gets created in the database, otherwise + # If the model is new, a record gets created in the database, otherwise # the existing record gets updated. # # With <tt>save!</tt> validations always run. If any of them fail # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations # for more information. # - # There's a series of callbacks associated with <tt>save!</tt>. If any of - # the <tt>before_*</tt> callbacks return +false+ the action is cancelled - # and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See + # By default, #save! also sets the +updated_at+/+updated_on+ attributes to + # the current time. However, if you supply <tt>touch: false</tt>, these + # timestamps will not be updated. + # + # There's a series of callbacks associated with #save!. If any of + # the <tt>before_*</tt> callbacks throws +:abort+ the action is cancelled + # and #save! raises ActiveRecord::RecordNotSaved. See # ActiveRecord::Callbacks for further details. # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save!(*) - create_or_update || raise(RecordNotSaved) + def save!(*args) + create_or_update(*args) || raise(RecordNotSaved.new(nil, self)) end # Deletes the record in the database and freezes this instance to @@ -136,6 +157,8 @@ module ActiveRecord # The row is simply removed with an SQL +DELETE+ statement on the # record's primary key, and no callbacks are executed. # + # Note that this will also delete records marked as <tt>readonly?</tt>. + # # To enforce the object's +before_destroy+ and +after_destroy+ # callbacks or any <tt>:dependent</tt> association # options, use <tt>#destroy</tt>. @@ -148,13 +171,14 @@ module ActiveRecord # Deletes the record in the database and freezes this instance to reflect # that no changes should be made (since they can't be persisted). # - # There's a series of callbacks associated with <tt>destroy</tt>. If - # the <tt>before_destroy</tt> callback return +false+ the action is cancelled - # and <tt>destroy</tt> returns +false+. See - # ActiveRecord::Callbacks for further details. + # There's a series of callbacks associated with #destroy. If the + # <tt>before_destroy</tt> callback throws +:abort+ the action is cancelled + # and #destroy returns +false+. + # See ActiveRecord::Callbacks for further details. def destroy - raise ReadOnlyRecord if readonly? + raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? destroy_associations + self.class.connection.add_transaction_record(self) destroy_row if persisted? @destroyed = true freeze @@ -163,12 +187,12 @@ module ActiveRecord # Deletes the record in the database and freezes this instance to reflect # that no changes should be made (since they can't be persisted). # - # There's a series of callbacks associated with <tt>destroy!</tt>. If - # the <tt>before_destroy</tt> callback return +false+ the action is cancelled - # and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See - # ActiveRecord::Callbacks for further details. + # There's a series of callbacks associated with #destroy!. If the + # <tt>before_destroy</tt> callback throws +:abort+ the action is cancelled + # and #destroy! raises ActiveRecord::RecordNotDestroyed. + # See ActiveRecord::Callbacks for further details. def destroy! - destroy || raise(ActiveRecord::RecordNotDestroyed) + destroy || raise(ActiveRecord::RecordNotDestroyed, self) end # Returns an instance of the specified +klass+ with the attributes of the @@ -222,8 +246,8 @@ module ActiveRecord def update_attribute(name, value) name = name.to_s verify_readonly_attribute(name) - send("#{name}=", value) - save(validate: false) + public_send("#{name}=", value) + save(validate: false) if changed? end # Updates the attributes of the model from the passed-in hash and saves the @@ -274,7 +298,8 @@ module ActiveRecord # This method raises an +ActiveRecord::ActiveRecordError+ when called on new # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) - raise ActiveRecordError, "cannot update on a new record object" unless persisted? + raise ActiveRecordError, "cannot update a new record" if new_record? + raise ActiveRecordError, "cannot update a destroyed record" if destroyed? attributes.each_key do |key| verify_readonly_attribute(key.to_s) @@ -328,7 +353,7 @@ module ActiveRecord # method toggles directly the underlying value without calling any setter. # Returns +self+. def toggle(attribute) - self[attribute] = !send("#{attribute}?") + self[attribute] = !public_send("#{attribute}?") self end @@ -399,9 +424,8 @@ module ActiveRecord self.class.unscoped { self.class.find(id) } end - @attributes.update(fresh_object.instance_variable_get('@attributes')) - - @column_types = self.class.column_types + @attributes = fresh_object.instance_variable_get('@attributes') + @new_record = false self end @@ -453,7 +477,7 @@ module ActiveRecord changes[self.class.locking_column] = increment_lock if locking_enabled? - changed_attributes.except!(*changes.keys) + clear_attribute_changes(changes.keys) primary_key = self.class.primary_key self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 else @@ -472,20 +496,12 @@ module ActiveRecord end def relation_for_destroy - pk = self.class.primary_key - column = self.class.columns_hash[pk] - substitute = self.class.connection.substitute_at(column, 0) - - relation = self.class.unscoped.where( - self.class.arel_table[pk].eq(substitute)) - - relation.bind_values = [[column, id]] - relation + self.class.unscoped.where(self.class.primary_key => id) end - def create_or_update - raise ReadOnlyRecord if readonly? - result = new_record? ? _create_record : _update_record + def create_or_update(*args) + raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? + result = new_record? ? _create_record : _update_record(*args) result != false end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index df8654e5c1..dcb2bd3d84 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Query Cache class QueryCache @@ -29,9 +28,10 @@ module ActiveRecord end def call(env) - enabled = ActiveRecord::Base.connection.query_cache_enabled + connection = ActiveRecord::Base.connection + enabled = connection.query_cache_enabled connection_id = ActiveRecord::Base.connection_id - ActiveRecord::Base.connection.enable_query_cache! + connection.enable_query_cache! response = @app.call(env) response[2] = Rack::BodyProxy.new(response[2]) do diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 39817703cd..4e597590e9 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -7,7 +7,7 @@ module ActiveRecord delegate :find_by, :find_by!, to: :all delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all delegate :find_each, :find_in_batches, to: :all - delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, + delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all @@ -37,26 +37,30 @@ module ActiveRecord # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] def find_by_sql(sql, binds = []) result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) - column_types = {} + column_types = result_set.column_types.dup + columns_hash.each_key { |k| column_types.delete k } + message_bus = ActiveSupport::Notifications.instrumenter - if result_set.respond_to? :column_types - column_types = result_set.column_types.except(*columns_hash.keys) - else - ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" - end + payload = { + record_count: result_set.length, + class_name: name + } - result_set.map { |record| instantiate(record, column_types) } + message_bus.instrument('instantiation.active_record', payload) do + result_set.map { |record| instantiate(record, column_types) } + end end # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. # The use of this method should be restricted to complicated SQL queries that can't be executed # using the ActiveRecord::Calculations class methods. Look into those before using this. # - # ==== Parameters + # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" + # # => 12 # - # * +sql+ - An SQL statement which should return a count query from the database, see the example below. + # ==== Parameters # - # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" + # * +sql+ - An SQL statement which should return a count query from the database, see the example above. def count_by_sql(sql) sql = sanitize_conditions(sql) connection.select_value(sql, "#{name} Count").to_i diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index a4ceacbf44..f1bdbc845c 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -36,8 +36,6 @@ module ActiveRecord config.eager_load_namespaces << ActiveRecord rake_tasks do - require "active_record/base" - namespace :db do task :load_config do ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 6dca206f2a..2591e7492d 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -28,12 +28,20 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.drop_current end + namespace :purge do + task :all => :load_config do + ActiveRecord::Tasks::DatabaseTasks.purge_all + end + end + + # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." + task :purge => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.purge_current + end + desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do - ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true - ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration| - ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope) - end + ActiveRecord::Tasks::DatabaseTasks.migrate db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration end @@ -82,22 +90,21 @@ db_namespace = namespace :db do desc 'Display status of migrations' task :status => [:environment, :load_config] do - unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) + unless ActiveRecord::SchemaMigration.table_exists? abort 'Schema migrations table does not exist yet.' end - db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") - db_list.map! { |version| ActiveRecord::SchemaMigration.normalize_migration_number(version) } - file_list = [] - ActiveRecord::Migrator.migrations_paths.each do |path| - Dir.foreach(path) do |file| - # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern - if match_data = /^(\d{3,})_(.+)\.rb$/.match(file) - version = ActiveRecord::SchemaMigration.normalize_migration_number(match_data[1]) - status = db_list.delete(version) ? 'up' : 'down' - file_list << [status, version, match_data[2].humanize] + db_list = ActiveRecord::SchemaMigration.normalized_versions + + file_list = + ActiveRecord::Migrator.migrations_paths.flat_map do |path| + # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern + Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do + version = ActiveRecord::SchemaMigration.normalize_migration_number($1) + status = db_list.delete(version) ? 'up' : 'down' + [status, version, $2.humanize] + end end - end - end + db_list.map! do |version| ['up', version, '********** NO FILE **********'] end @@ -105,8 +112,8 @@ db_namespace = namespace :db do puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration| - puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}" + (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name| + puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts end @@ -178,17 +185,22 @@ db_namespace = namespace :db do task :load => [:environment, :load_config] do require 'active_record/fixtures' - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path - fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact + fixtures_dir = if ENV['FIXTURES_DIR'] + File.join base_dir, ENV['FIXTURES_DIR'] + else + base_dir + end - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(',') : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| - ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file) - end + fixture_files = if ENV['FIXTURES'] + ENV['FIXTURES'].split(',') + else + # The use of String#[] here is to support namespaced fixtures + Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] } + end + + ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files) end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." @@ -200,16 +212,11 @@ db_namespace = namespace :db do puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end - + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) - data.keys.each do |key| + data.each_key do |key| key_id = ActiveRecord::FixtureSet.identify(key) if key == label || key_id == id.to_i @@ -233,8 +240,8 @@ db_namespace = namespace :db do end desc 'Load a schema.rb file into the database' - task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby, ENV['SCHEMA']) + task :load => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) end task :load_if_ruby => ['db:create', :environment] do @@ -262,9 +269,9 @@ db_namespace = namespace :db do end namespace :structure do - desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' + desc 'Dump the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' task :dump => [:environment, :load_config] do - filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") + filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) @@ -279,8 +286,8 @@ db_namespace = namespace :db do end desc "Recreate the databases from the structure.sql file" - task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE']) + task :load => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA']) end task :load_if_sql => ['db:create', :environment] do @@ -298,7 +305,7 @@ db_namespace = namespace :db do end # desc "Recreate the test database from the current schema" - task :load => %w(db:test:deprecated db:test:purge) do + task :load => %w(db:test:purge) do case ActiveRecord::Base.schema_format when :ruby db_namespace["test:load_schema"].invoke @@ -308,12 +315,11 @@ db_namespace = namespace :db do end # desc "Recreate the test database from an existent schema.rb file" - task :load_schema => %w(db:test:deprecated db:test:purge) do + task :load_schema => %w(db:test:purge) do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) ActiveRecord::Schema.verbose = false - db_namespace["schema:load"].invoke + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA'] ensure if should_reconnect ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) @@ -322,13 +328,8 @@ db_namespace = namespace :db do end # desc "Recreate the test database from an existent structure.sql file" - task :load_structure => %w(db:test:deprecated db:test:purge) do - begin - ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test']) - db_namespace["structure:load"].invoke - ensure - ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil) - end + task :load_structure => %w(db:test:purge) do + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] end # desc "Recreate the test database from a fresh schema" @@ -348,12 +349,12 @@ db_namespace = namespace :db do task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure) # desc "Empty the test database" - task :purge => %w(db:test:deprecated environment load_config) do + task :purge => %w(environment load_config) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] end # desc 'Check for pending migrations and load the test schema' - task :prepare => %w(db:test:deprecated environment load_config) do + task :prepare => %w(environment load_config) do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke end @@ -365,7 +366,7 @@ namespace :railties do namespace :install do # desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2" task :migrations => :'db:load_config' do - to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip } + to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map(&:strip) railties = {} Rails.application.migration_railties.each do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index b3ddfd63d4..ce78f1756d 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -1,4 +1,3 @@ - module ActiveRecord module ReadonlyAttributes extend ActiveSupport::Concern @@ -12,7 +11,7 @@ module ActiveRecord # Attributes listed as readonly will be used to create a new record but update operations will # ignore these fields. def attr_readonly(*attributes) - self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || []) + self._attr_readonly = Set.new(attributes.map(&:to_s)) + (self._attr_readonly || []) end # Returns an array of all the attributes that have been specified as readonly. diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 51c96373ee..dab5a502a5 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,4 +1,5 @@ require 'thread' +require 'active_support/core_ext/string/filters' module ActiveRecord # = Active Record Reflection @@ -13,14 +14,21 @@ module ActiveRecord end def self.create(macro, name, scope, options, ar) - case macro - when :has_many, :belongs_to, :has_one - klass = options[:through] ? ThroughReflection : AssociationReflection - when :composed_of - klass = AggregateReflection - end - - klass.new(macro, name, scope, options, ar) + klass = case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end + + reflection = klass.new(name, scope, options, ar) + options[:through] ? ThroughReflection.new(reflection) : reflection end def self.add_reflection(ar, name, reflection) @@ -31,9 +39,9 @@ module ActiveRecord ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) end - # \Reflection enables to interrogate Active Record classes and objects - # about their associations and aggregations. This information can, - # for example, be used in a form builder that takes an Active Record object + # \Reflection enables the ability to examine the associations and aggregations of + # Active Record classes and objects. This information, for example, + # can be used in a form builder that takes an Active Record object # and creates input fields for all of the attributes depending on their type # and displays the associations to other objects. # @@ -55,7 +63,7 @@ module ActiveRecord # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. # - # Account.reflections # => {balance: AggregateReflection} + # Account.reflections # => {"balance" => AggregateReflection} # # @api public def reflections @@ -110,26 +118,67 @@ module ActiveRecord end end + # Holds all the methods that are shared between MacroReflection, AssociationReflection + # and ThroughReflection + class AbstractReflection # :nodoc: + def table_name + klass.table_name + end + + # Returns a new, unsaved instance of the associated class. +attributes+ will + # be passed to the class's constructor. + def build_association(attributes, &block) + klass.new(attributes, &block) + end + + def quoted_table_name + klass.quoted_table_name + end + + def primary_key_type + klass.type_for_attribute(klass.primary_key) + end + + # Returns the class name for the macro. + # + # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> + # <tt>has_many :clients</tt> returns <tt>'Client'</tt> + def class_name + @class_name ||= (options[:class_name] || derive_class_name).to_s + end + + JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: + + def join_keys(association_klass) + JoinKeys.new(foreign_key, active_record_primary_key) + end + + def constraints + scope_chain.flatten + end + + def alias_candidate(name) + "#{plural_name}_#{name}" + end + end + # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. # # MacroReflection - # AggregateReflection # AssociationReflection - # ThroughReflection - class MacroReflection + # AggregateReflection + # HasManyReflection + # HasOneReflection + # BelongsToReflection + # ThroughReflection + class MacroReflection < AbstractReflection # Returns the name of the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt> # <tt>has_many :clients</tt> returns <tt>:clients</tt> attr_reader :name - # Returns the macro type. - # - # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt> - # <tt>has_many :clients</tt> returns <tt>:has_many</tt> - attr_reader :macro - attr_reader :scope # Returns the hash of options used for the macro. @@ -142,8 +191,7 @@ module ActiveRecord attr_reader :plural_name # :nodoc: - def initialize(macro, name, scope, options, active_record) - @macro = macro + def initialize(name, scope, options, active_record) @name = name @scope = scope @options = options @@ -167,15 +215,11 @@ module ActiveRecord # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class # <tt>has_many :clients</tt> returns the Client class def klass - @klass ||= class_name.constantize + @klass ||= compute_class(class_name) end - # Returns the class name for the macro. - # - # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> - # <tt>has_many :clients</tt> returns <tt>'Client'</tt> - def class_name - @class_name ||= (options[:class_name] || derive_class_name).to_s + def compute_class(name) + name.constantize end # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute, @@ -188,23 +232,6 @@ module ActiveRecord active_record == other_aggregation.active_record end - JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: - - def join_keys(assoc_klass) - if source_macro == :belongs_to - if polymorphic? - reflection_key = association_primary_key(assoc_klass) - else - reflection_key = association_primary_key - end - reflection_foreign_key = foreign_key - else - reflection_key = foreign_key - reflection_foreign_key = active_record_primary_key - end - JoinKeys.new(reflection_key, reflection_foreign_key) - end - private def derive_class_name name.to_s.camelize @@ -237,17 +264,20 @@ module ActiveRecord # a new association object. Use +build_association+ or +create_association+ # instead. This allows plugins to hook into association object creation. def klass - @klass ||= active_record.send(:compute_type, class_name) + @klass ||= compute_class(class_name) + end + + def compute_class(name) + active_record.send(:compute_type, name) end attr_reader :type, :foreign_type attr_accessor :parent_reflection # [:name, Reflection] - def initialize(macro, name, scope, options, active_record) + def initialize(name, scope, options, active_record) super - @collection = macro == :has_many @automatic_inverse_of = nil - @type = options[:as] && "#{options[:as]}_type" + @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type") @foreign_type = options[:foreign_type] || "#{name}_type" @constructable = calculate_constructable(macro, options) @association_scope_cache = {} @@ -257,31 +287,17 @@ module ActiveRecord def association_scope_cache(conn, owner) key = conn.prepared_statements if polymorphic? - key = [key, owner.read_attribute(@foreign_type)] + key = [key, owner._read_attribute(@foreign_type)] end @association_scope_cache[key] ||= @scope_lock.synchronize { @association_scope_cache[key] ||= yield } end - # Returns a new, unsaved instance of the associated class. +attributes+ will - # be passed to the class's constructor. - def build_association(attributes, &block) - klass.new(attributes, &block) - end - def constructable? # :nodoc: @constructable end - def table_name - klass.table_name - end - - def quoted_table_name - klass.quoted_table_name - end - def join_table @join_table ||= options[:join_table] || derive_join_table end @@ -290,10 +306,6 @@ module ActiveRecord @foreign_key ||= options[:foreign_key] || derive_foreign_key end - def primary_key_column - klass.columns_hash[klass.primary_key] - end - def association_foreign_key @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key end @@ -331,19 +343,17 @@ module ActiveRecord return unless scope if scope.arity > 0 - ActiveSupport::Deprecation.warn <<-WARNING -The association scope '#{name}' is instance dependent (the scope block takes an argument). -Preloading happens before the individual instances are created. This means that there is no instance -being passed to the association scope. This will most likely result in broken or incorrect behavior. -Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future. - WARNING + raise ArgumentError, <<-MSG.squish + The association scope '#{name}' is instance dependent (the scope + block takes an argument). Preloading instance dependent scopes is + not supported. + MSG end end alias :check_eager_loadable! :check_preloadable! - def join_id_for(owner) #:nodoc: - key = (source_macro == :belongs_to) ? foreign_key : active_record_primary_key - owner[key] + def join_id_for(owner) # :nodoc: + owner[active_record_primary_key] end def through_reflection @@ -370,8 +380,6 @@ Joining, Preloading and eager loading of these associations is deprecated and wi scope ? [[scope]] : [[]] end - alias :source_macro :macro - def has_inverse? inverse_name end @@ -392,11 +400,16 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end end + # Returns the macro type. + # + # <tt>has_many :clients</tt> returns <tt>:has_many</tt> + def macro; raise NotImplementedError; end + # Returns whether or not this association reflection is for a collection # association. Returns +true+ if the +macro+ is either +has_many+ or # +has_and_belongs_to_many+, +false+ otherwise. def collection? - @collection + false end # Returns whether or not the association should be validated as part of @@ -413,14 +426,10 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end # Returns +true+ if +self+ is a +belongs_to+ reflection. - def belongs_to? - macro == :belongs_to - end + def belongs_to?; false; end # Returns +true+ if +self+ is a +has_one+ reflection. - def has_one? - macro == :has_one - end + def has_one?; false; end def association_class case macro @@ -487,7 +496,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # returns either nil or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) - inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name).to_sym + inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym begin reflection = klass._reflect_on_association(inverse_name) @@ -552,7 +561,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end def derive_join_table - [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + ModelSchema.derive_join_table_name active_record.table_name, klass.table_name end def primary_key(klass) @@ -560,22 +569,72 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end end - class HasAndBelongsToManyReflection < AssociationReflection #:nodoc: - def initialize(macro, name, scope, options, active_record) + class HasManyReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :has_many; end + + def collection?; true; end + end + + class HasOneReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :has_one; end + + def has_one?; true; end + end + + class BelongsToReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :belongs_to; end + + def belongs_to?; true; end + + def join_keys(association_klass) + key = polymorphic? ? association_primary_key(association_klass) : association_primary_key + JoinKeys.new(key, foreign_key) + end + + def join_id_for(owner) # :nodoc: + owner[foreign_key] + end + end + + class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) super - @collection = true + end + + def macro; :has_and_belongs_to_many; end + + def collection? + true end end # Holds all the meta-data about a :through association as it was specified # in the Active Record class. - class ThroughReflection < AssociationReflection #:nodoc: + class ThroughReflection < AbstractReflection #:nodoc: + attr_reader :delegate_reflection delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :type, :to => :source_reflection - def initialize(macro, name, scope, options, active_record) - super - @source_reflection_name = options[:source] + def initialize(delegate_reflection) + @delegate_reflection = delegate_reflection + @klass = delegate_reflection.options[:class] + @source_reflection_name = delegate_reflection.options[:source] + end + + def klass + @klass ||= delegate_reflection.compute_class(class_name) end # Returns the source of the through reflection. It checks both a singularized @@ -593,7 +652,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.source_reflection - # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags"> + # # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags"> # def source_reflection through_reflection.klass._reflect_on_association(source_reflection_name) @@ -609,7 +668,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.through_reflection - # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings"> + # # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings"> # def through_reflection active_record._reflect_on_association(options[:through]) @@ -629,13 +688,18 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # 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>] + # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>, + # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>] # def chain @chain ||= begin a = source_reflection.chain b = through_reflection.chain + + if options[:source_type] + b[0] = PolymorphicReflection.new(b[0], self) + end + chain = a + b chain[0] = self # Use self so we don't lose the information from :source_type chain @@ -671,8 +735,11 @@ Joining, Preloading and eager loading of these associations is deprecated and wi through_scope_chain = through_reflection.scope_chain.map(&:dup) if options[:source_type] - through_scope_chain.first << - through_reflection.klass.where(foreign_type => options[:source_type]) + type = foreign_type + source_type = options[:source_type] + through_scope_chain.first << lambda { |object| + where(type => source_type) + } end # Recursively fill out the rest of the array from the through reflection @@ -680,9 +747,8 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end end - # The macro used by the source association - def source_macro - source_reflection.source_macro + def join_keys(association_klass) + source_reflection.join_keys(association_klass) end # A through association is nested if there would be more than one join table @@ -717,7 +783,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi def source_reflection_name # :nodoc: return @source_reflection_name if @source_reflection_name - names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq + names = [name.to_s.singularize, name].collect(&:to_sym).uniq names = names.find_all { |n| through_reflection.klass._reflect_on_association(n) } @@ -725,15 +791,13 @@ Joining, Preloading and eager loading of these associations is deprecated and wi if names.length > 1 example_options = options.dup example_options[:source] = source_reflection_names.first - ActiveSupport::Deprecation.warn <<-eowarn -Ambiguous source reflection for through association. Please specify a :source -directive on your declaration like: - - class #{active_record.name} < ActiveRecord::Base - #{macro} :#{name}, #{example_options} - end - - eowarn + ActiveSupport::Deprecation.warn \ + "Ambiguous source reflection for through association. Please " \ + "specify a :source directive on your declaration like:\n" \ + "\n" \ + " class #{active_record.name} < ActiveRecord::Base\n" \ + " #{macro} :#{name}, #{example_options}\n" \ + " end" end @source_reflection_name = names.first @@ -747,13 +811,21 @@ directive on your declaration like: through_reflection.options end + def join_id_for(owner) # :nodoc: + source_reflection.join_id_for(owner) + end + def check_validity! if through_reflection.nil? raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) end if through_reflection.polymorphic? - raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) + if has_one? + raise HasOneAssociationPolymorphicThroughError.new(active_record.name, self) + else + raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) + end end if source_reflection.nil? @@ -775,17 +847,109 @@ directive on your declaration like: check_validity_of_inverse! end + def constraints + scope_chain = source_reflection.constraints + scope_chain << scope if scope + scope_chain + end + protected - def actual_source_reflection # FIXME: this is a horrible name - source_reflection.actual_source_reflection - end + def actual_source_reflection # FIXME: this is a horrible name + source_reflection.send(:actual_source_reflection) + end + + def primary_key(klass) + klass.primary_key || raise(UnknownPrimaryKey.new(klass)) + end private def derive_class_name # get the class_name of the belongs_to association of the through reflection options[:source_type] || source_reflection.class_name end + + delegate_methods = AssociationReflection.public_instance_methods - + public_instance_methods + + delegate(*delegate_methods, to: :delegate_reflection) + + end + + class PolymorphicReflection < ThroughReflection # :nodoc: + def initialize(reflection, previous_reflection) + @reflection = reflection + @previous_reflection = previous_reflection + end + + def klass + @reflection.klass + end + + def scope + @reflection.scope + end + + def table_name + @reflection.table_name + end + + def plural_name + @reflection.plural_name + end + + def join_keys(association_klass) + @reflection.join_keys(association_klass) + end + + def type + @reflection.type + end + + def constraints + [source_type_info] + end + + def source_type_info + type = @previous_reflection.foreign_type + source_type = @previous_reflection.options[:source_type] + lambda { |object| where(type => source_type) } + end + end + + class RuntimeReflection < PolymorphicReflection # :nodoc: + attr_accessor :next + + def initialize(reflection, association) + @reflection = reflection + @association = association + end + + def klass + @association.klass + end + + def table_name + klass.table_name + end + + def constraints + @reflection.constraints + end + + def source_type_info + @reflection.source_type_info + end + + def alias_candidate(name) + "#{plural_name}_#{name}_join" + end + + def alias_name + Arel::Table.new(table_name) + end + + def all_includes; yield; end end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index cef40be7ce..6bbec7c0c0 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,40 +1,39 @@ # -*- coding: utf-8 -*- -require 'arel/collectors/bind' +require "arel/collectors/bind" module ActiveRecord # = Active Record Relation class Relation - JoinOperation = Struct.new(:relation, :join_class, :on) - MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :where, :having, :bind, :references, + :order, :joins, :references, :extending, :unscope] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :uniq] + CLAUSE_METHODS = [:where, :having, :from] INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] - VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation - attr_reader :table, :klass, :loaded + attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded - def initialize(klass, table, values = {}) + def initialize(klass, table, predicate_builder, values = {}) @klass = klass @table = table @values = values @offsets = {} @loaded = false + @predicate_builder = predicate_builder end def initialize_copy(other) # This method is a hot spot, so for now, use Hash[] to dup the hash. # https://bugs.ruby-lang.org/issues/7166 @values = Hash[@values] - @values[:bind] = @values[:bind].dup if @values.key? :bind reset end @@ -81,22 +80,26 @@ module ActiveRecord scope.unscope!(where: @klass.inheritance_column) end - um = scope.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key) + relation = scope.where(@klass.primary_key => (id_was || id)) + bvs = binds + relation.bound_attributes + um = relation + .arel + .compile_update(substitutes, @klass.primary_key) @klass.connection.update( um, 'SQL', - binds) + bvs, + ) end def substitute_values(values) # :nodoc: - substitutes = values.sort_by { |arel_attr,_| arel_attr.name } - binds = substitutes.map do |arel_attr, value| - [@klass.columns_hash[arel_attr.name], value] + binds = values.map do |arel_attr, value| + QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name)) end - substitutes.each_with_index do |tuple, i| - tuple[1] = @klass.connection.substitute_at(binds[i][0], i) + substitutes = values.map do |(arel_attr, _)| + [arel_attr, connection.substitute_at(klass.columns_hash[arel_attr.name])] end [substitutes, binds] @@ -242,6 +245,11 @@ module ActiveRecord @records end + # Serializes the relation objects Array. + def encode_with(coder) + coder.represent_seq(nil, to_a) + end + def as_json(options = nil) #:nodoc: to_a.as_json(options) end @@ -263,6 +271,15 @@ module ActiveRecord end end + # Returns true if there are no records. + def none? + if block_given? + to_a.none? { |*block_args| yield(*block_args) } + else + empty? + end + end + # Returns true if there are any records. def any? if block_given? @@ -272,6 +289,15 @@ module ActiveRecord end end + # Returns true if there is exactly one record. + def one? + if block_given? + to_a.one? { |*block_args| yield(*block_args) } + else + limit_value ? to_a.one? : size == 1 + end + end + # Returns true if there is more than one record. def many? if block_given? @@ -297,10 +323,11 @@ module ActiveRecord klass.current_scope = previous end - # Updates all records with details given if they match a set of conditions supplied, limits and order can - # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the - # database. It does not instantiate the involved models and it does not trigger Active Record callbacks - # or validations. + # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE + # statement and sends it straight to the database. It does not instantiate the involved models and it does not + # trigger Active Record callbacks or validations. Values passed to `update_all` will not go through + # ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL + # database. # # ==== Parameters # @@ -319,7 +346,7 @@ module ActiveRecord def update_all(updates) raise ArgumentError, "Empty list of attributes to change" if updates.blank? - stmt = Arel::UpdateManager.new(arel.engine) + stmt = Arel::UpdateManager.new stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) stmt.table(table) @@ -333,8 +360,7 @@ module ActiveRecord stmt.wheres = arel.constraints end - bvs = bind_values + arel.bind_values - @klass.connection.update stmt, 'SQL', bvs + @klass.connection.update stmt, 'SQL', bound_attributes end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -353,9 +379,21 @@ module ActiveRecord # # Updates multiple records # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } # Person.update(people.keys, people.values) - def update(id, attributes) + # + # # Updates multiple records from the result of a relation + # people = Person.where(group: 'expert') + # people.update(group: 'masters') + # + # Note: Updating a large number of records will run a + # UPDATE query for each record, which may cause a performance + # issue. So if it is not needed to run callbacks for each update, it is + # preferred to use <tt>update_all</tt> for updating all records using + # a single query. + def update(id = :all, attributes) if id.is_a?(Array) id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } + elsif id == :all + to_a.each { |record| record.update(attributes) } else object = find(id) object.update(attributes) @@ -392,7 +430,7 @@ module ActiveRecord if conditions where(conditions).destroy_all else - to_a.each {|object| object.destroy }.tap { reset } + to_a.each(&:destroy).tap { reset } end end @@ -446,8 +484,10 @@ module ActiveRecord invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method| if MULTI_VALUE_METHODS.include?(method) send("#{method}_values").any? - else + elsif SINGLE_VALUE_METHODS.include?(method) send("#{method}_value") + elsif CLAUSE_METHODS.include?(method) + send("#{method}_clause").any? end } if invalid_methods.any? @@ -457,7 +497,7 @@ module ActiveRecord if conditions where(conditions).delete_all else - stmt = Arel::DeleteManager.new(arel.engine) + stmt = Arel::DeleteManager.new stmt.from(table) if joins_values.any? @@ -466,7 +506,7 @@ module ActiveRecord stmt.wheres = arel.constraints end - affected = @klass.connection.delete(stmt, 'SQL', bind_values) + affected = @klass.connection.delete(stmt, 'SQL', bound_attributes) reset affected @@ -536,10 +576,10 @@ module ActiveRecord find_with_associations { |rel| relation = rel } end - arel = relation.arel - binds = (arel.bind_values + relation.bind_values).dup - binds.map! { |bv| connection.quote(*bv.reverse) } - collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new) + binds = relation.bound_attributes + binds = connection.prepare_binds_for_database(binds) + binds.map! { |value| connection.quote(value) } + collect = visitor.accept(relation.arel.ast, Arel::Collectors::Bind.new) collect.substitute_binds(binds).join end end @@ -549,22 +589,7 @@ module ActiveRecord # User.where(name: 'Oscar').where_values_hash # # => {name: "Oscar"} def where_values_hash(relation_table_name = table_name) - equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node| - node.left.relation.name == relation_table_name - } - - binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] - - Hash[equalities.map { |where| - name = where.left.name - [name, binds.fetch(name.to_s) { - case where.right - when Array then where.right.map(&:val) - else - where.right.val - end - }] - }] + where_clause.to_h(relation_table_name) end def scope_for_create @@ -627,21 +652,25 @@ module ActiveRecord private def exec_queries - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bound_attributes) preload = preload_values preload += includes_values unless eager_loading? - preloader = ActiveRecord::Associations::Preloader.new + preloader = build_preloader preload.each do |associations| preloader.preload @records, associations end - @records.each { |record| record.readonly! } if readonly_value + @records.each(&:readonly!) if readonly_value @loaded = true @records end + def build_preloader + ActiveRecord::Associations::Preloader.new + end + def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| if join.is_a?(Arel::Nodes::StringJoin) @@ -654,7 +683,7 @@ module ActiveRecord joined_tables += [table.name, table.table_alias] # always convert table names to downcase as in Oracle quoted table names are in uppercase - joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq + joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq (references_values - joined_tables).any? end @@ -663,7 +692,7 @@ module ActiveRecord return [] if string.blank? # always convert table names to downcase as in Oracle quoted table names are in uppercase # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries - string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] + string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_'] end end end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 29fc150b3d..e07580a563 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Batches # Looping through a collection of records from the database @@ -28,37 +27,46 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:start+ option on that worker). + # (by setting the +:begin_at+ and +:end_at+ option on each worker). # # # Let's process for a batch of 2000 records, skipping the first 2000 rows - # Person.find_each(start: 2000, batch_size: 2000) do |person| + # Person.find_each(begin_at: 2000, batch_size: 2000) do |person| # person.party_all_night! # end # # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # work. This also means that this method only works with integer-based - # primary keys. + # work. This also means that this method only works when the primary key is + # orderable (e.g. an integer or string). # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_each(options = {}) + def find_each(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) + if start + begin_at = start + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing `start` value to find_each is deprecated, and will be removed in Rails 5.1. + Please pass `begin_at` instead. + MSG + end if block_given? - find_in_batches(options) do |records| + find_in_batches(begin_at: begin_at, end_at: end_at, batch_size: batch_size) do |records| records.each { |record| yield record } end else - enum_for :find_each, options do - options[:start] ? where(table[primary_key].gteq(options[:start])).size : size + enum_for(:find_each, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do + relation = self + apply_limits(relation, begin_at, end_at).size end end end - # Yields each batch of records that was found by the find +options+ as + # Yields each batch of records that was found by the find options as # an array. # # Person.where("age > 21").find_in_batches do |group| @@ -78,34 +86,38 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:start+ option on that worker). + # (by setting the +:begin_at+ and +:end_at+ option on each worker). # # # Let's process the next 2000 records - # Person.find_in_batches(start: 2000, batch_size: 2000) do |group| + # Person.find_in_batches(begin_at: 2000, batch_size: 2000) do |group| # group.each { |person| person.party_all_night! } # end # # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # work. This also means that this method only works with integer-based - # primary keys. + # work. This also means that this method only works when the primary key is + # orderable (e.g. an integer or string). # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_in_batches(options = {}) - options.assert_valid_keys(:start, :batch_size) + def find_in_batches(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) + if start + begin_at = start + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing `start` value to find_in_batches is deprecated, and will be removed in Rails 5.1. + Please pass `begin_at` instead. + MSG + end relation = self - start = options[:start] - batch_size = options[:batch_size] || 1000 - unless block_given? - return to_enum(:find_in_batches, options) do - total = start ? where(table[primary_key].gteq(start)).size : size + return to_enum(:find_in_batches, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do + total = apply_limits(relation, begin_at, end_at).size (total - 1).div(batch_size) + 1 end end @@ -115,7 +127,8 @@ module ActiveRecord end relation = relation.reorder(batch_order).limit(batch_size) - records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a + relation = apply_limits(relation, begin_at, end_at) + records = relation.to_a while records.any? records_size = records.size @@ -132,6 +145,12 @@ module ActiveRecord private + def apply_limits(relation, begin_at, end_at) + relation = relation.where(table[primary_key].gteq(begin_at)) if begin_at + relation = relation.where(table[primary_key].lteq(end_at)) if end_at + relation + end + def batch_order "#{quoted_table_name}.#{quoted_primary_key} ASC" end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 028e4d80ab..4a4de86d48 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -20,28 +20,31 @@ module ActiveRecord # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } # + # If +count+ is used with +group+ for multiple columns, it returns a Hash whose + # keys are an array containing the individual values of each column and the value + # of each key would be the +count+. + # + # Article.group(:status, :category).count + # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, + # ["published", "business"]=>0, ["published", "technology"]=>2} + # # If +count+ is used with +select+, it will count the selected columns: # # Person.select(:age).count # # => counts the number of different age values # # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ - # between databases. In invalid cases, an error from the databsae is thrown. - def count(column_name = nil, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - column_name, options = nil, column_name if column_name.is_a?(Hash) - calculate(:count, column_name, options) + # between databases. In invalid cases, an error from the database is thrown. + def count(column_name = nil) + calculate(:count, column_name) end # Calculates the average value on a given column. Returns +nil+ if there's # no row. See +calculate+ for examples with options. # # Person.average(:age) # => 35.8 - def average(column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - calculate(:average, column_name, options) + def average(column_name) + calculate(:average, column_name) end # Calculates the minimum value on a given column. The value is returned @@ -49,10 +52,8 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.minimum(:age) # => 7 - def minimum(column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - calculate(:minimum, column_name, options) + def minimum(column_name) + calculate(:minimum, column_name) end # Calculates the maximum value on a given column. The value is returned @@ -60,10 +61,8 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.maximum(:age) # => 93 - def maximum(column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - calculate(:maximum, column_name, options) + def maximum(column_name) + calculate(:maximum, column_name) end # Calculates the sum of values on a given column. The value is returned @@ -106,17 +105,15 @@ module ActiveRecord # Person.group(:last_name).having("min(age) > 17").minimum(:age) # # Person.sum("2 * age") - def calculate(operation, column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. + def calculate(operation, column_name) if column_name.is_a?(Symbol) && attribute_alias?(column_name) column_name = attribute_alias(column_name) end if has_include?(column_name) - construct_relation_for_association_calculations.calculate(operation, column_name, options) + construct_relation_for_association_calculations.calculate(operation, column_name) else - perform_calculation(operation, column_name, options) + perform_calculation(operation, column_name) end end @@ -169,17 +166,8 @@ module ActiveRecord relation.select_values = column_names.map { |cn| columns_hash.key?(cn) ? arel_table[cn] : cn } - result = klass.connection.select_all(relation.arel, nil, bind_values) - columns = result.columns.map do |key| - klass.column_types.fetch(key) { - result.column_types.fetch(key) { result.identity_type } - } - end - - result = result.rows.map do |values| - columns.zip(values).map { |column, value| column.type_cast_from_database value } - end - columns.one? ? result.map!(&:first) : result + result = klass.connection.select_all(relation.arel, nil, bound_attributes) + result.cast_values(klass.attribute_types) end end @@ -197,9 +185,7 @@ module ActiveRecord eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?)) end - def perform_calculation(operation, column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. + def perform_calculation(operation, column_name) operation = operation.to_s.downcase # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) @@ -241,31 +227,28 @@ module ActiveRecord column_alias = column_name - bind_values = nil - if operation == "count" && (relation.limit_value || relation.offset_value) # Shortcut when limit is zero. return 0 if relation.limit_value == 0 query_builder = build_count_subquery(relation, column_name, distinct) - bind_values = query_builder.bind_values + relation.bind_values else column = aggregate_column(column_name) select_value = operation_over_aggregate_column(column, operation, distinct) column_alias = select_value.alias + column_alias ||= @klass.connection.column_name_for_operation(operation, select_value) relation.select_values = [select_value] query_builder = relation.arel - bind_values = query_builder.bind_values + relation.bind_values end - result = @klass.connection.select_all(query_builder, nil, bind_values) + result = @klass.connection.select_all(query_builder, nil, bound_attributes) row = result.first value = row && row.values.first column = result.column_types.fetch(column_alias) do - column_for(column_name) + type_for(column_name) end type_cast_calculated_value(value, column, operation) @@ -275,7 +258,7 @@ module ActiveRecord group_attrs = group_values if group_attrs.first.respond_to?(:to_sym) - association = @klass._reflect_on_association(group_attrs.first.to_sym) + association = @klass._reflect_on_association(group_attrs.first) associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attrs) else @@ -303,7 +286,7 @@ module ActiveRecord operation, distinct).as(aggregate_alias) ] - select_values += select_values unless having_values.empty? + select_values += select_values unless having_clause.empty? select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| if field.respond_to?(:as) @@ -317,25 +300,25 @@ module ActiveRecord relation.group_values = group relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation, nil, bind_values) + calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } - key_records = association.klass.base_class.find(key_ids) + key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids) key_records = Hash[key_records.map { |r| [r.id, r] }] end Hash[calculated_data.map do |row| key = group_columns.map { |aliaz, col_name| column = calculated_data.column_types.fetch(aliaz) do - column_for(col_name) + type_for(col_name) end type_cast_calculated_value(row[aliaz], column) } key = key.first if key.size == 1 key = key_records[key] if associated - column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) } + column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)] end] end @@ -362,24 +345,20 @@ module ActiveRecord @klass.connection.table_alias_for(table_name) end - def column_for(field) + def type_for(field) field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last - @klass.columns_hash[field_name] + @klass.type_for_attribute(field_name) end - def type_cast_calculated_value(value, column, operation = nil) + def type_cast_calculated_value(value, type, operation = nil) case operation when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || 0, column) + when 'sum' then type.deserialize(value || 0) when 'average' then value.respond_to?(:to_d) ? value.to_d : value - else type_cast_using_column(value, column) + else type.deserialize(value) end end - def type_cast_using_column(value, column) - column ? column.type_cast_from_database(value) : value - end - # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). def select_for_count if select_values.present? @@ -395,11 +374,9 @@ module ActiveRecord aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) relation.select_values = [aliased_column] - arel = relation.arel - subquery = arel.as(subquery_alias) + subquery = relation.arel.as(subquery_alias) sm = Arel::SelectManager.new relation.engine - sm.bind_values = arel.bind_values select_value = operation_over_aggregate_column(column_alias, 'count', distinct) sm.project(select_value).from(subquery) end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 50f4d5c7ab..d4a8823cfe 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,6 +1,5 @@ require 'set' require 'active_support/concern' -require 'active_support/deprecation' module ActiveRecord module Delegation # :nodoc: diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 0c9c761f97..fb47d915ff 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,4 +1,5 @@ require 'active_support/deprecation' +require 'active_support/core_ext/string/filters' module ActiveRecord module FinderMethods @@ -81,12 +82,16 @@ module ActiveRecord # Post.find_by "published_at < ?", 2.weeks.ago def find_by(*args) where(*args).take + rescue RangeError + nil end # Like <tt>find_by</tt>, except that if no record is found, raises # an <tt>ActiveRecord::RecordNotFound</tt> error. def find_by!(*args) where(*args).take! + rescue RangeError + raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value" end # Gives a record (or N records if a parameter is supplied) without any implied @@ -103,7 +108,7 @@ module ActiveRecord # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. Note that <tt>take!</tt> accepts no arguments. def take! - take or raise RecordNotFound + take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end # Find the first record (or first N records if a parameter is supplied). @@ -138,7 +143,7 @@ module ActiveRecord # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. Note that <tt>first!</tt> accepts no arguments. def first! - first or raise RecordNotFound + find_nth! 0 end # Find the last record (or last N records if a parameter is supplied). @@ -171,7 +176,7 @@ module ActiveRecord # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. Note that <tt>last!</tt> accepts no arguments. def last! - last or raise RecordNotFound + last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end # Find the second record. @@ -187,7 +192,7 @@ module ActiveRecord # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def second! - second or raise RecordNotFound + find_nth! 1 end # Find the third record. @@ -203,7 +208,7 @@ module ActiveRecord # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def third! - third or raise RecordNotFound + find_nth! 2 end # Find the fourth record. @@ -219,7 +224,7 @@ module ActiveRecord # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def fourth! - fourth or raise RecordNotFound + find_nth! 3 end # Find the fifth record. @@ -235,7 +240,7 @@ module ActiveRecord # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def fifth! - fifth or raise RecordNotFound + find_nth! 4 end # Find the forty-second record. Also known as accessing "the reddit". @@ -251,7 +256,7 @@ module ActiveRecord # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def forty_two! - forty_two or raise RecordNotFound + find_nth! 41 end # Returns +true+ if a record exists in the table that matches the +id+ or @@ -284,8 +289,10 @@ module ActiveRecord def exists?(conditions = :none) if Base === conditions conditions = conditions.id - ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `exists?`." \ - "Please pass the id of the object by calling `.id`" + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `exists?`. + Please pass the id of the object by calling `.id` + MSG end return false if !conditions @@ -300,11 +307,11 @@ module ActiveRecord relation = relation.where(conditions) else unless conditions == :none - relation = where(primary_key => conditions) + relation = relation.where(primary_key => conditions) end end - connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false + connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false end # This method is called whenever no records are found with either a single @@ -316,7 +323,7 @@ module ActiveRecord # the expected number of results should be provided in the +expected_size+ # argument. def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc: - conditions = arel.where_sql + conditions = arel.where_sql(@klass.arel_engine) conditions = " [#{conditions}]" if conditions if Array(ids).size == 1 @@ -358,7 +365,7 @@ module ActiveRecord [] else arel = relation.arel - rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values) + rows = connection.select_all(arel, 'SQL', relation.bound_attributes) join_dependency.instantiate(rows, aliases) end end @@ -390,7 +397,7 @@ module ActiveRecord else if relation.limit_value limited_ids = limited_ids_for(relation) - limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids)) + limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end relation.except(:limit, :offset) end @@ -401,13 +408,14 @@ module ActiveRecord "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values) relation = relation.except(:select).select(values).distinct! + arel = relation.arel - id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values) + id_rows = @klass.connection.select_all(arel, 'SQL', relation.bound_attributes) id_rows.map {|row| row[primary_key]} end def using_limitable_reflections?(reflections) - reflections.none? { |r| r.collection? } + reflections.none?(&:collection?) end protected @@ -429,19 +437,20 @@ module ActiveRecord else find_some(ids) end + rescue RangeError + raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" end def find_one(id) if ActiveRecord::Base === id id = id.id - ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \ - "Please pass the id of the object by calling `.id`" + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `find`. + Please pass the id of the object by calling `.id` + MSG end - column = columns_hash[primary_key] - substitute = connection.substitute_at(column, bind_values.length) - relation = where(table[primary_key].eq(substitute)) - relation.bind_values += [[column, id]] + relation = where(primary_key => id) record = relation.take raise_record_not_found_exception!(id, 0, 1) unless record @@ -450,7 +459,7 @@ module ActiveRecord end def find_some(ids) - result = where(table[primary_key].in(ids)).to_a + result = where(primary_key => ids).to_a expected_size = if limit_value && ids.size > limit_value @@ -488,6 +497,10 @@ module ActiveRecord end end + def find_nth!(index) + find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + end + def find_nth_with_limit(offset, limit) relation = if order_values.empty? && primary_key order(arel_table[primary_key].asc) diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb new file mode 100644 index 0000000000..a93952fa30 --- /dev/null +++ b/activerecord/lib/active_record/relation/from_clause.rb @@ -0,0 +1,32 @@ +module ActiveRecord + class Relation + class FromClause + attr_reader :value, :name + + def initialize(value, name) + @value = value + @name = name + end + + def binds + if value.is_a?(Relation) + value.bound_attributes + else + [] + end + end + + def merge(other) + self + end + + def empty? + value.nil? + end + + def self.empty + new(nil, nil) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index ac41d0aa80..65b607ff1c 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -13,7 +13,7 @@ module ActiveRecord @hash = hash end - def merge + def merge #:nodoc: Merger.new(relation, other).merge end @@ -22,7 +22,7 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.create(relation.klass, relation.table) + other = Relation.create(relation.klass, relation.table, relation.predicate_builder) hash.each { |k, v| if k == :joins if Hash === v @@ -49,9 +49,9 @@ module ActiveRecord @other = other end - NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS + - Relation::MULTI_VALUE_METHODS - - [:joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc: + NORMAL_VALUES = Relation::VALUE_METHODS - + Relation::CLAUSE_METHODS - + [:joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: def normal_values NORMAL_VALUES @@ -75,6 +75,7 @@ module ActiveRecord merge_multi_values merge_single_values + merge_clauses merge_joins relation @@ -83,12 +84,12 @@ module ActiveRecord private def merge_joins - return if values[:joins].blank? + return if other.joins_values.blank? if other.klass == relation.klass - relation.joins!(*values[:joins]) + relation.joins!(*other.joins_values) else - joins_dependency, rest = values[:joins].partition do |join| + joins_dependency, rest = other.joins_values.partition do |join| case join when Hash, Symbol, Array true @@ -107,74 +108,30 @@ module ActiveRecord end def merge_multi_values - lhs_wheres = relation.where_values - rhs_wheres = values[:where] || [] - - lhs_binds = relation.bind_values - rhs_binds = values[:bind] || [] - - removed, kept = partition_overwrites(lhs_wheres, rhs_wheres) - - where_values = kept + rhs_wheres - bind_values = filter_binds(lhs_binds, removed) + rhs_binds - - conn = relation.klass.connection - bv_index = 0 - where_values.map! do |node| - if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right - substitute = conn.substitute_at(bind_values[bv_index].first, bv_index) - bv_index += 1 - Arel::Nodes::Equality.new(node.left, substitute) - else - node - end - end - - relation.where_values = where_values - relation.bind_values = bind_values - - if values[:reordering] + if other.reordering_value # override any order specified in the original relation - relation.reorder! values[:order] - elsif values[:order] + relation.reorder! other.order_values + elsif other.order_values # merge in order_values from relation - relation.order! values[:order] + relation.order! other.order_values end - relation.extend(*values[:extending]) unless values[:extending].blank? + relation.extend(*other.extending_values) unless other.extending_values.blank? end def merge_single_values - relation.from_value = values[:from] unless relation.from_value - relation.lock_value = values[:lock] unless relation.lock_value + relation.lock_value ||= other.lock_value - unless values[:create_with].blank? - relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with]) + unless other.create_with_value.blank? + relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value) end end - def filter_binds(lhs_binds, removed_wheres) - return lhs_binds if removed_wheres.empty? - - set = Set.new removed_wheres.map { |x| x.left.name.to_s } - lhs_binds.dup.delete_if { |col,_| set.include? col.name } - end - - # Remove equalities from the existing relation with a LHS which is - # present in the relation being merged in. - # returns [things_to_remove, things_to_keep] - def partition_overwrites(lhs_wheres, rhs_wheres) - if lhs_wheres.empty? || rhs_wheres.empty? - return [[], lhs_wheres] - end - - nodes = rhs_wheres.find_all do |w| - w.respond_to?(:operator) && w.operator == :== - end - seen = Set.new(nodes) { |node| node.left } - - lhs_wheres.partition do |w| - w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left) + def merge_clauses + CLAUSE_METHODS.each do |name| + clause = relation.send("#{name}_clause") + other_clause = other.send("#{name}_clause") + relation.send("#{name}_clause=", clause.merge(other_clause)) end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index eff5c8f09c..43e9afe853 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,82 +1,49 @@ module ActiveRecord class PredicateBuilder # :nodoc: - @handlers = [] - - autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler' - autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler' - - def self.resolve_column_aliases(klass, hash) - hash = hash.dup - hash.keys.grep(Symbol) do |key| - if klass.attribute_alias? key - hash[klass.attribute_alias(key)] = hash.delete key - end - end - hash + require 'active_record/relation/predicate_builder/array_handler' + require 'active_record/relation/predicate_builder/association_query_handler' + require 'active_record/relation/predicate_builder/base_handler' + require 'active_record/relation/predicate_builder/basic_object_handler' + require 'active_record/relation/predicate_builder/class_handler' + require 'active_record/relation/predicate_builder/range_handler' + require 'active_record/relation/predicate_builder/relation_handler' + + delegate :resolve_column_aliases, to: :table + + def initialize(table) + @table = table + @handlers = [] + + register_handler(BasicObject, BasicObjectHandler.new(self)) + register_handler(Class, ClassHandler.new(self)) + register_handler(Base, BaseHandler.new(self)) + register_handler(Range, RangeHandler.new(self)) + register_handler(Relation, RelationHandler.new) + register_handler(Array, ArrayHandler.new(self)) + register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) end - def self.build_from_hash(klass, attributes, default_table) - queries = [] - - attributes.each do |column, value| - table = default_table - - if value.is_a?(Hash) - if value.empty? - queries << '1=0' - else - table = Arel::Table.new(column, default_table.engine) - association = klass._reflect_on_association(column.to_sym) - - value.each do |k, v| - queries.concat expand(association && association.klass, table, k, v) - end - end - else - column = column.to_s - - if column.include?('.') - table_name, column = column.split('.', 2) - table = Arel::Table.new(table_name, default_table.engine) - end - - queries.concat expand(klass, table, column, value) - end - end - - queries + def build_from_hash(attributes) + attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + expand_from_hash(attributes) end - def self.expand(klass, table, column, value) - queries = [] + def create_binds(attributes) + attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + create_binds_for_hash(attributes) + end + def expand(column, value) # Find the foreign key when using queries such as: # Post.where(author: author) # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && reflection = klass._reflect_on_association(column.to_sym) - if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value) - queries << build(table[reflection.foreign_type], base_class) - end - - column = reflection.foreign_key + if table.associated_with?(column) + value = AssociationQueryValue.new(table.associated_table(column), value) end - queries << build(table[column], value) - queries - end - - def self.polymorphic_base_class_from_value(value) - case value - when Relation - value.klass.base_class - when Array - val = value.compact.first - val.class.base_class if val.is_a?(Base) - when Base - value.class.base_class - end + build(table.arel_attribute(column), value) end def self.references(attributes) @@ -101,26 +68,82 @@ module ActiveRecord # ) # end # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler) - def self.register_handler(klass, handler) + def register_handler(klass, handler) @handlers.unshift([klass, handler]) end - register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) }) - # FIXME: I think we need to deprecate this behavior - register_handler(Class, ->(attribute, value) { attribute.eq(value.name) }) - register_handler(Base, ->(attribute, value) { attribute.eq(value.id) }) - register_handler(Range, ->(attribute, value) { attribute.in(value) }) - register_handler(Relation, RelationHandler.new) - register_handler(Array, ArrayHandler.new) - - def self.build(attribute, value) + def build(attribute, value) handler_for(value).call(attribute, value) end - private_class_method :build - def self.handler_for(object) + protected + + attr_reader :table + + def expand_from_hash(attributes) + return ["1=0"] if attributes.empty? + + attributes.flat_map do |key, value| + if value.is_a?(Hash) + associated_predicate_builder(key).expand_from_hash(value) + else + expand(key, value) + end + end + end + + + def create_binds_for_hash(attributes) + result = attributes.dup + binds = [] + + attributes.each do |column_name, value| + case value + when Hash + attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value) + result[column_name] = attrs + binds += bvs + when Relation + binds += value.bound_attributes + else + if can_be_bound?(column_name, value) + result[column_name] = Arel::Nodes::BindParam.new + binds << Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + end + end + end + + [result, binds] + end + + private + + def associated_predicate_builder(association_name) + self.class.new(table.associated_table(association_name)) + end + + def convert_dot_notation_to_hash(attributes) + dot_notation = attributes.keys.select { |s| s.include?(".") } + + dot_notation.each do |key| + table_name, column_name = key.split(".") + value = attributes.delete(key) + attributes[table_name] ||= {} + + attributes[table_name] = attributes[table_name].merge(column_name => value) + end + + attributes + end + + def handler_for(object) @handlers.detect { |klass, _| klass === object }.last end - private_class_method :handler_for + + def can_be_bound?(column_name, value) + !value.nil? && + handler_for(value).is_a?(BasicObjectHandler) && + !table.associated_with?(column_name) + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 78dba8be06..95dbd6a77f 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -1,30 +1,39 @@ module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: - def call(attribute, value) - return attribute.in([]) if value.empty? + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + def call(attribute, value) values = value.map { |x| x.is_a?(Base) ? x.id : x } - ranges, values = values.partition { |v| v.is_a?(Range) } nils, values = values.partition(&:nil?) + return attribute.in([]) if values.empty? && nils.empty? + + ranges, values = values.partition { |v| v.is_a?(Range) } + values_predicate = case values.length when 0 then NullPredicate - when 1 then attribute.eq(values.first) + when 1 then predicate_builder.build(attribute, values.first) else attribute.in(values) end unless nils.empty? - values_predicate = values_predicate.or(attribute.eq(nil)) + values_predicate = values_predicate.or(predicate_builder.build(attribute, nil)) end - array_predicates = ranges.map { |range| attribute.in(range) } - array_predicates << values_predicate + array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) } + array_predicates.unshift(values_predicate) array_predicates.inject { |composite, predicate| composite.or(predicate) } end - module NullPredicate + protected + + attr_reader :predicate_builder + + module NullPredicate # :nodoc: def self.or(other) other end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb new file mode 100644 index 0000000000..159889d3b8 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -0,0 +1,78 @@ +module ActiveRecord + class PredicateBuilder + class AssociationQueryHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + queries = {} + + table = value.associated_table + if value.base_class + queries[table.association_foreign_type] = value.base_class.name + end + + queries[table.association_foreign_key] = value.ids + predicate_builder.build_from_hash(queries) + end + + protected + + attr_reader :predicate_builder + end + + class AssociationQueryValue # :nodoc: + attr_reader :associated_table, :value + + def initialize(associated_table, value) + @associated_table = associated_table + @value = value + end + + def ids + case value + when Relation + value.select(primary_key) + when Array + value.map { |v| convert_to_id(v) } + else + convert_to_id(value) + end + end + + def base_class + if associated_table.polymorphic_association? + @base_class ||= polymorphic_base_class_from_value + end + end + + private + + def primary_key + associated_table.association_primary_key(base_class) + end + + def polymorphic_base_class_from_value + case value + when Relation + value.klass.base_class + when Array + val = value.compact.first + val.class.base_class if val.is_a?(Base) + when Base + value.class.base_class + end + end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key) + else + value + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb new file mode 100644 index 0000000000..6fa5b16f73 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class BaseHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + predicate_builder.build(attribute, value.id) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb new file mode 100644 index 0000000000..6cec75dc0a --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class BasicObjectHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + attribute.eq(value) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb new file mode 100644 index 0000000000..ed313fc9d4 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb @@ -0,0 +1,27 @@ +module ActiveRecord + class PredicateBuilder + class ClassHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + print_deprecation_warning + predicate_builder.build(attribute, value.name) + end + + protected + + attr_reader :predicate_builder + + private + + def print_deprecation_warning + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a class as a value in an Active Record query is deprecated and + will be removed. Pass a string instead. + MSG + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb new file mode 100644 index 0000000000..1b3849e3ad --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class RangeHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + attribute.between(value) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb index 618fa3cdd9..063150958a 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb @@ -6,7 +6,7 @@ module ActiveRecord value = value.select(value.klass.arel_table[value.klass.primary_key]) end - attribute.in(value.arel.ast) + attribute.in(value.arel) end end end diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb new file mode 100644 index 0000000000..e69319b4de --- /dev/null +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -0,0 +1,19 @@ +require 'active_record/attribute' + +module ActiveRecord + class Relation + class QueryAttribute < Attribute + def type_cast(value) + value + end + + def value_for_database + @value_for_database ||= super + end + + def with_cast_value(value) + QueryAttribute.new(name, value, type) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 1262b2c291..7514401072 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,9 +1,16 @@ -require 'active_support/core_ext/array/wrap' +require "active_record/relation/from_clause" +require "active_record/relation/query_attribute" +require "active_record/relation/where_clause" +require "active_record/relation/where_clause_factory" +require 'active_model/forbidden_attributes_protection' +require 'active_support/core_ext/string/filters' module ActiveRecord module QueryMethods extend ActiveSupport::Concern + include ActiveModel::ForbiddenAttributesProtection + # WhereChain objects act as placeholder for queries in which #where does not have any parameter. # In this case, #where must be chained with #not to return a new relation. class WhereChain @@ -35,38 +42,24 @@ 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 - when NilClass - raise ArgumentError, 'Invalid argument for .where.not(), got nil.' - when Arel::Nodes::In - Arel::Nodes::NotIn.new(rel.left, rel.right) - when Arel::Nodes::Equality - Arel::Nodes::NotEqual.new(rel.left, rel.right) - when String - Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(rel)) - else - Arel::Nodes::Not.new(rel) - end - end + where_clause = @scope.send(:where_clause_factory).build(opts, rest) @scope.references!(PredicateBuilder.references(opts)) if Hash === opts - @scope.where_values += where_value + @scope.where_clause += where_clause.invert @scope end end Relation::MULTI_VALUE_METHODS.each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_values # def select_values - @values[:#{name}] || [] # @values[:select] || [] - end # end - # - def #{name}_values=(values) # def select_values=(values) - raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded - check_cached_relation - @values[:#{name}] = values # @values[:select] = values - end # end + def #{name}_values # def select_values + @values[:#{name}] || [] # @values[:select] || [] + end # end + # + def #{name}_values=(values) # def select_values=(values) + assert_mutability! # assert_mutability! + @values[:#{name}] = values # @values[:select] = values + end # end CODE end @@ -81,21 +74,27 @@ module ActiveRecord Relation::SINGLE_VALUE_METHODS.each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}_value=(value) # def readonly_value=(value) - raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded - check_cached_relation + assert_mutability! # assert_mutability! @values[:#{name}] = value # @values[:readonly] = value end # end CODE end - def check_cached_relation # :nodoc: - if defined?(@arel) && @arel - @arel = nil - ActiveSupport::Deprecation.warn <<-WARNING -Modifying already cached Relation. The cache will be reset. -Use a cloned Relation to prevent this warning. -WARNING - end + Relation::CLAUSE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_clause # def where_clause + @values[:#{name}] || new_#{name}_clause # @values[:where] || new_where_clause + end # end + # + def #{name}_clause=(value) # def where_clause=(value) + assert_mutability! # assert_mutability! + @values[:#{name}] = value # @values[:where] = value + end # end + CODE + end + + def bound_attributes + from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds end def create_with_value # :nodoc: @@ -400,9 +399,8 @@ WARNING raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key." end - Array(target_value).each do |val| - where_unscoping(val) - end + target_values = Array(target_value).map(&:to_s) + self.where_clause = where_clause.except(*target_values) end else raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example." @@ -423,27 +421,16 @@ WARNING # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id def joins(*args) check_if_method_has_arguments!(:joins, args) - - args.compact! - args.flatten! - spawn.joins!(*args) end def joins!(*args) # :nodoc: + args.compact! + args.flatten! self.joins_values += args self end - def bind(value) - spawn.bind!(value) - end - - def bind!(value) # :nodoc: - self.bind_values += [value] - self - end - # Returns a new relation, which is the result of filtering the current relation # according to the conditions in the arguments. # @@ -574,9 +561,12 @@ WARNING end def where!(opts, *rest) # :nodoc: - references!(PredicateBuilder.references(opts)) if Hash === opts + if Hash === opts + opts = sanitize_forbidden_attributes(opts) + references!(PredicateBuilder.references(opts)) + end - self.where_values += build_where(opts, rest) + self.where_clause += where_clause_factory.build(opts, rest) self end @@ -592,6 +582,37 @@ WARNING unscope(where: conditions.keys).where(conditions) end + # Returns a new relation, which is the logical union of this relation and the one passed as an + # argument. + # + # The two relations must be structurally compatible: they must be scoping the same model, and + # they must differ only by +where+ (if no +group+ has been defined) or +having+ (if a +group+ is + # present). Neither relation may have a +limit+, +offset+, or +uniq+ set. + # + # Post.where("id = 1").or(Post.where("id = 2")) + # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) + # + def or(other) + spawn.or!(other) + end + + def or!(other) # :nodoc: + unless structurally_compatible_for_or?(other) + raise ArgumentError, 'Relation passed to #or must be structurally compatible' + end + + self.where_clause = self.where_clause.or(other.where_clause) + self.having_clause = self.having_clause.or(other.having_clause) + + self + end + + private def structurally_compatible_for_or?(other) # :nodoc: + Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } && + (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } && + (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") } + end + # Allows to specify a HAVING clause. Note that you can't use HAVING # without also specifying a GROUP clause. # @@ -603,7 +624,7 @@ WARNING def having!(opts, *rest) # :nodoc: references!(PredicateBuilder.references(opts)) if Hash === opts - self.having_values += build_where(opts, rest) + self.having_clause += having_clause_factory.build(opts, rest) self end @@ -683,11 +704,11 @@ WARNING # end # def none - extending(NullRelation) + where("1=0").extending!(NullRelation) end def none! # :nodoc: - extending!(NullRelation) + where!("1=0").extending!(NullRelation) end # Sets readonly attributes for the returned relation. If value is @@ -723,7 +744,13 @@ WARNING end def create_with!(value) # :nodoc: - self.create_with_value = value ? create_with_value.merge(value) : {} + if value + value = sanitize_forbidden_attributes(value) + self.create_with_value = create_with_value.merge(value) + else + self.create_with_value = {} + end + self end @@ -745,7 +772,7 @@ WARNING end def from!(value, subquery_name = nil) # :nodoc: - self.from_value = [value, subquery_name] + self.from_clause = Relation::FromClause.new(value, subquery_name) self end @@ -846,37 +873,30 @@ WARNING private + def assert_mutability! + raise ImmutableRelation if @loaded + raise ImmutableRelation if defined?(@arel) && @arel + end + def build_arel - arel = Arel::SelectManager.new(table.engine, table) + arel = Arel::SelectManager.new(table) build_joins(arel, joins_values.flatten) unless joins_values.empty? - collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds - - arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty? - + arel.where(where_clause.ast) unless where_clause.empty? + arel.having(having_clause.ast) unless having_clause.empty? arel.take(connection.sanitize_limit(limit_value)) if limit_value arel.skip(offset_value.to_i) if offset_value - - arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty? + arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? build_order(arel) - build_select(arel, select_values.uniq) + build_select(arel) arel.distinct(distinct_value) - arel.from(build_from) if from_value + arel.from(build_from) unless from_clause.empty? arel.lock(lock_value) if lock_value - # Reorder bind indexes if joins produced bind values - if arel.bind_values.any? - bvs = arel.bind_values + bind_values - arel.ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i| - column = bvs[i].first - bp.replace connection.substitute_at(column, i) - end - end - arel end @@ -885,112 +905,36 @@ WARNING raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." end - single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope) - unscope_code = "#{scope}_value#{'s' unless single_val_method}=" + clause_method = Relation::CLAUSE_METHODS.include?(scope) + multi_val_method = Relation::MULTI_VALUE_METHODS.include?(scope) + if clause_method + unscope_code = "#{scope}_clause=" + else + unscope_code = "#{scope}_value#{'s' if multi_val_method}=" + end case scope when :order result = [] - when :where - self.bind_values = [] else - result = [] unless single_val_method + result = [] if multi_val_method end self.send(unscope_code, result) end - def where_unscoping(target_value) - target_value = target_value.to_s - - where_values.reject! do |rel| - case rel - when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual - subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) - subrelation.name == target_value - end - end - - bind_values.reject! { |col,_| col.name == target_value } - end - - def custom_join_ast(table, joins) - joins = joins.reject(&:blank?) - - return [] if joins.empty? - - joins.map! do |join| - case join - when Array - join = Arel.sql(join.join(' ')) if array_of_strings?(join) - when String - join = Arel.sql(join) - end - table.create_string_join(join) - end - end - - def collapse_wheres(arel, wheres) - predicates = wheres.map do |where| - next where if ::Arel::Nodes::Equality === where - where = Arel.sql(where) if String === where - Arel::Nodes::Grouping.new(where) - end - - arel.where(Arel::Nodes::And.new(predicates)) if predicates.present? - end - - def build_where(opts, other = []) - case opts - when String, Array - [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] - when Hash - opts = PredicateBuilder.resolve_column_aliases(klass, opts) - - bv_len = bind_values.length - tmp_opts, bind_values = create_binds(opts, bv_len) - self.bind_values += bind_values - - attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts) - attributes.values.grep(ActiveRecord::Relation) do |rel| - self.bind_values += rel.bind_values - end - - PredicateBuilder.build_from_hash(klass, attributes, table) - else - [opts] - end - end - - def create_binds(opts, idx) - bindable, non_binds = opts.partition do |column, value| - case value - when String, Integer, ActiveRecord::StatementCache::Substitute - @klass.columns_hash.include? column.to_s - else - false - end - end - - new_opts = {} - binds = [] - - bindable.each_with_index do |(column,value), index| - binds.push [@klass.columns_hash[column.to_s], value] - new_opts[column] = connection.substitute_at(column, index + idx) - end - - non_binds.each { |column,value| new_opts[column] = value } - - [new_opts, binds] + def association_for_table(table_name) + table_name = table_name.to_s + @klass._reflect_on_association(table_name) || + @klass._reflect_on_association(table_name.singularize) end def build_from - opts, name = from_value + opts = from_clause.value + name = from_clause.name case opts when Relation name ||= 'subquery' - self.bind_values = opts.bind_values + self.bind_values opts.arel.as(name.to_s) else opts @@ -1012,13 +956,14 @@ WARNING raise 'unknown class: %s' % join.class.name end end + buckets.default = [] - association_joins = buckets[:association_join] || [] - stashed_association_joins = buckets[:stashed_join] || [] - join_nodes = (buckets[:join_node] || []).uniq - string_joins = (buckets[:string_join] || []).map(&:strip).uniq + association_joins = buckets[:association_join] + stashed_association_joins = buckets[:stashed_join] + join_nodes = buckets[:join_node].uniq + string_joins = buckets[:string_join].map(&:strip).uniq - join_list = join_nodes + custom_join_ast(manager, string_joins) + join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins) join_dependency = ActiveRecord::Associations::JoinDependency.new( @klass, @@ -1038,17 +983,31 @@ WARNING manager end - def build_select(arel, selects) - if !selects.empty? - expanded_select = selects.map do |field| - columns_hash.key?(field.to_s) ? arel_table[field] : field - end - arel.project(*expanded_select) + def convert_join_strings_to_ast(table, joins) + joins + .flatten + .reject(&:blank?) + .map { |join| table.create_string_join(Arel.sql(join)) } + end + + def build_select(arel) + if select_values.any? + arel.project(*arel_columns(select_values.uniq)) else arel.project(@klass.arel_table[Arel.star]) end end + def arel_columns(columns) + columns.map do |field| + if (Symbol === field || String === field) && columns_hash.key?(field.to_s) + arel_table[field] + else + field + end + end + end + def reverse_sql_order(order_query) order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? @@ -1067,10 +1026,6 @@ WARNING end end - def array_of_strings?(o) - o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) } - end - def build_order(arel) orders = order_values.uniq orders.reject!(&:blank?) @@ -1137,5 +1092,19 @@ WARNING raise ArgumentError, "The method .#{method_name}() must contain arguments." end end + + def new_where_clause + Relation::WhereClause.empty + end + alias new_having_clause new_where_clause + + def where_clause_factory + @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder) + end + alias having_clause_factory where_clause_factory + + def new_from_clause + Relation::FromClause.empty + end end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 57d66bce4b..70da37fa84 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -32,7 +32,7 @@ module ActiveRecord elsif other spawn.merge!(other) else - self + raise ArgumentError, "invalid argument: #{other.inspect}." end end @@ -58,16 +58,13 @@ module ActiveRecord # Post.order('id asc').only(:where) # discards the order condition # Post.order('id asc').only(:where, :order) # uses the specified order def only(*onlies) - if onlies.any? { |o| o == :where } - onlies << :bind - end relation_with values.slice(*onlies) end private def relation_with(values) # :nodoc: - result = Relation.create(klass, table, values) + result = Relation.create(klass, table, predicate_builder, values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb new file mode 100644 index 0000000000..f9b9e640ec --- /dev/null +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -0,0 +1,173 @@ +module ActiveRecord + class Relation + class WhereClause # :nodoc: + attr_reader :binds + + delegate :any?, :empty?, to: :predicates + + def initialize(predicates, binds) + @predicates = predicates + @binds = binds + end + + def +(other) + WhereClause.new( + predicates + other.predicates, + binds + other.binds, + ) + end + + def merge(other) + WhereClause.new( + predicates_unreferenced_by(other) + other.predicates, + non_conflicting_binds(other) + other.binds, + ) + end + + def except(*columns) + WhereClause.new( + predicates_except(columns), + binds_except(columns), + ) + end + + def or(other) + if empty? + self + elsif other.empty? + other + else + WhereClause.new( + [ast.or(other.ast)], + binds + other.binds + ) + end + end + + def to_h(table_name = nil) + equalities = predicates.grep(Arel::Nodes::Equality) + if table_name + equalities = equalities.select do |node| + node.left.relation.name == table_name + end + end + + binds = self.binds.map { |attr| [attr.name, attr.value] }.to_h + + equalities.map { |node| + name = node.left.name + [name, binds.fetch(name.to_s) { + case node.right + when Array then node.right.map(&:val) + when Arel::Nodes::Casted, Arel::Nodes::Quoted + node.right.val + end + }] + }.to_h + end + + def ast + Arel::Nodes::And.new(predicates_with_wrapped_sql_literals) + end + + def ==(other) + other.is_a?(WhereClause) && + predicates == other.predicates && + binds == other.binds + end + + def invert + WhereClause.new(inverted_predicates, binds) + end + + def self.empty + new([], []) + end + + protected + + attr_reader :predicates + + def referenced_columns + @referenced_columns ||= begin + equality_nodes = predicates.select { |n| equality_node?(n) } + Set.new(equality_nodes, &:left) + end + end + + private + + def predicates_unreferenced_by(other) + predicates.reject do |n| + equality_node?(n) && other.referenced_columns.include?(n.left) + end + end + + def equality_node?(node) + node.respond_to?(:operator) && node.operator == :== + end + + def non_conflicting_binds(other) + conflicts = referenced_columns & other.referenced_columns + conflicts.map! { |node| node.name.to_s } + binds.reject { |attr| conflicts.include?(attr.name) } + end + + def inverted_predicates + predicates.map { |node| invert_predicate(node) } + end + + def invert_predicate(node) + case node + when NilClass + raise ArgumentError, 'Invalid argument for .where.not(), got nil.' + when Arel::Nodes::In + Arel::Nodes::NotIn.new(node.left, node.right) + when Arel::Nodes::Equality + Arel::Nodes::NotEqual.new(node.left, node.right) + when String + Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node)) + else + Arel::Nodes::Not.new(node) + end + end + + def predicates_except(columns) + predicates.reject do |node| + case node + when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual + subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) + columns.include?(subrelation.name.to_s) + end + end + end + + def binds_except(columns) + binds.reject do |attr| + columns.include?(attr.name) + end + end + + def predicates_with_wrapped_sql_literals + non_empty_predicates.map do |node| + if Arel::Nodes::Equality === node + node + else + wrap_sql_literal(node) + end + end + end + + def non_empty_predicates + predicates - [''] + end + + def wrap_sql_literal(node) + if ::String === node + node = Arel.sql(node) + end + Arel::Nodes::Grouping.new(node) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb new file mode 100644 index 0000000000..0430922be3 --- /dev/null +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -0,0 +1,34 @@ +module ActiveRecord + class Relation + class WhereClauseFactory + def initialize(klass, predicate_builder) + @klass = klass + @predicate_builder = predicate_builder + end + + def build(opts, other) + binds = [] + + case opts + when String, Array + parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] + when Hash + attributes = predicate_builder.resolve_column_aliases(opts) + attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) + + attributes, binds = predicate_builder.create_binds(attributes) + + parts = predicate_builder.build_from_hash(attributes) + else + parts = [opts] + end + + WhereClause.new(parts, binds) + end + + protected + + attr_reader :klass, :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 8c29c69a9b..500c478e65 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -42,12 +42,8 @@ module ActiveRecord @column_types = column_types end - def identity_type # :nodoc: - IDENTITY_TYPE - end - - def column_type(name) - @column_types[name] || identity_type + def length + @rows.length end def each @@ -82,6 +78,15 @@ module ActiveRecord hash_rows.last end + def cast_values(type_overrides = {}) # :nodoc: + types = columns.map { |name| column_type(name, type_overrides) } + result = rows.map do |values| + types.zip(values).map { |type, value| type.deserialize(value) } + end + + columns.one? ? result.map!(&:first) : result + end + def initialize_copy(other) @columns = columns.dup @rows = rows.dup @@ -91,6 +96,12 @@ module ActiveRecord private + def column_type(name, type_overrides = {}) + type_overrides.fetch(name) do + column_types.fetch(name, IDENTITY_TYPE) + end + end + def hash_rows @hash_rows ||= begin diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 1aa93ffbb3..c7f55ebaa1 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -3,14 +3,11 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - def quote_value(value, column) #:nodoc: - connection.quote(value, column) - end - # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. def sanitize(object) #:nodoc: connection.quote(object) end + alias_method :quote_value, :sanitize protected @@ -72,42 +69,18 @@ module ActiveRecord expanded_attrs end - # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause. - # { name: "foo'bar", group_id: 4 } - # # => "name='foo''bar' and group_id= 4" - # { status: nil, group_id: [1,2,3] } - # # => "status IS NULL and group_id IN (1,2,3)" - # { age: 13..18 } - # # => "age BETWEEN 13 AND 18" - # { 'other_records.id' => 7 } - # # => "`other_records`.`id` = 7" - # { other_records: { id: 7 } } - # # => "`other_records`.`id` = 7" - # And for value objects on a composed_of relationship: - # { address: Address.new("123 abc st.", "chicago") } - # # => "address_street='123 abc st.' and address_city='chicago'" - def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) - attrs = PredicateBuilder.resolve_column_aliases self, attrs - attrs = expand_hash_conditions_for_aggregates(attrs) - - table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) - PredicateBuilder.build_from_hash(self, attrs, table).map { |b| - connection.visitor.compile b - }.join(' AND ') - end - alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions - # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. # { status: nil, group_id: 1 } # # => "status = NULL , group_id = 1" def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| - "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}" + value = type_for_attribute(attr.to_s).serialize(value) + "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" end.join(', ') end - # Sanitizes a +string+ so that it is safe to use within a sql + # Sanitizes a +string+ so that it is safe to use within an SQL # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%" def sanitize_sql_like(string, escape_character = "\\") pattern = Regexp.union(escape_character, "%", "_") @@ -134,7 +107,7 @@ module ActiveRecord raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) bound = values.dup c = connection - statement.gsub('?') do + statement.gsub(/\?/) do replace_bind_variable(bound.shift, c) end end @@ -159,10 +132,8 @@ module ActiveRecord end end - def quote_bound_value(value, c = connection, column = nil) #:nodoc: - if column - c.quote(value, column) - elsif value.respond_to?(:map) && !value.acts_like?(:string) + def quote_bound_value(value, c = connection) #:nodoc: + if value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? c.quote(nil) else @@ -182,7 +153,7 @@ module ActiveRecord # TODO: Deprecate this def quoted_id - self.class.quote_value(id, column_for_attribute(self.class.primary_key)) + self.class.quote_value(@attributes[self.class.primary_key].value_for_database) end end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 4bfd0167a4..0a5546a760 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Schema # diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index e055d571ab..da95920571 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -44,7 +44,6 @@ module ActiveRecord def initialize(connection, options = {}) @connection = connection - @types = @connection.native_database_types @version = Migrator::current_version rescue nil @options = options end @@ -91,16 +90,17 @@ HEADER end def tables(stream) - @connection.tables.sort.each do |tbl| - next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| - case ignored - when String; remove_prefix_and_suffix(tbl) == ignored - when Regexp; remove_prefix_and_suffix(tbl) =~ ignored - else - raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' - end + sorted_tables = @connection.tables.sort + + sorted_tables.each do |table_name| + table(table_name, stream) unless ignored?(table_name) + end + + # dump foreign keys at the end to make sure all dependent tables exist. + if @connection.supports_foreign_keys? + sorted_tables.each do |tbl| + foreign_keys(tbl, stream) unless ignored?(tbl) end - table(tbl, stream) end end @@ -110,32 +110,31 @@ HEADER tbl = StringIO.new # first dump primary key column - if @connection.respond_to?(:pk_and_sequence_for) - pk, _ = @connection.pk_and_sequence_for(table) - elsif @connection.respond_to?(:primary_key) - pk = @connection.primary_key(table) - end + pk = @connection.primary_key(table) tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" pkcol = columns.detect { |c| c.name == pk } if pkcol if pk != 'id' tbl.print %Q(, primary_key: "#{pk}") - elsif pkcol.sql_type == 'uuid' - tbl.print ", id: :uuid" - tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function + end + pkcolspec = @connection.column_spec_for_primary_key(pkcol) + if pkcolspec + pkcolspec.each do |key, value| + tbl.print ", #{key}: #{value}" + end end else tbl.print ", id: false" end - tbl.print ", force: true" + tbl.print ", force: :cascade" tbl.puts " do |t|" # then dump all non-primary key columns column_specs = columns.map do |column| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) next if column.name == pk - @connection.column_spec(column, @types) + @connection.column_spec(column) end.compact # find all migration keys used in this table @@ -186,34 +185,67 @@ HEADER if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| statement_parts = [ - ('add_index ' + remove_prefix_and_suffix(index.table).inspect), + "add_index #{remove_prefix_and_suffix(index.table).inspect}", index.columns.inspect, - ('name: ' + index.name.inspect), + "name: #{index.name.inspect}", ] statement_parts << 'unique: true' if index.unique index_lengths = (index.lengths || []).compact - statement_parts << ('length: ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty? + statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any? + + index_orders = index.orders || {} + statement_parts << "order: #{index.orders.inspect}" if index_orders.any? + statement_parts << "where: #{index.where.inspect}" if index.where + statement_parts << "using: #{index.using.inspect}" if index.using + statement_parts << "type: #{index.type.inspect}" if index.type + + " #{statement_parts.join(', ')}" + end + + stream.puts add_index_statements.sort.join("\n") + stream.puts + end + end + + def foreign_keys(table, stream) + if (foreign_keys = @connection.foreign_keys(table)).any? + add_foreign_key_statements = foreign_keys.map do |foreign_key| + parts = [ + "add_foreign_key #{remove_prefix_and_suffix(foreign_key.from_table).inspect}", + remove_prefix_and_suffix(foreign_key.to_table).inspect, + ] - index_orders = (index.orders || {}) - statement_parts << ('order: ' + index.orders.inspect) unless index_orders.empty? + if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table) + parts << "column: #{foreign_key.column.inspect}" + end - statement_parts << ('where: ' + index.where.inspect) if index.where + if foreign_key.custom_primary_key? + parts << "primary_key: #{foreign_key.primary_key.inspect}" + end - statement_parts << ('using: ' + index.using.inspect) if index.using + if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/ + parts << "name: #{foreign_key.name.inspect}" + end - statement_parts << ('type: ' + index.type.inspect) if index.type + parts << "on_update: #{foreign_key.on_update.inspect}" if foreign_key.on_update + parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete - ' ' + statement_parts.join(', ') + " #{parts.join(', ')}" end - stream.puts add_index_statements.sort.join("\n") - stream.puts + stream.puts add_foreign_key_statements.sort.join("\n") end end def remove_prefix_and_suffix(table) table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") end + + def ignored?(table_name) + ['schema_migrations', ignore_tables].flatten.any? do |ignored| + ignored === remove_prefix_and_suffix(table_name) + end + end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 3a004d58c9..b5038104ac 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -43,6 +43,10 @@ module ActiveRecord def normalize_migration_number(number) "%.3d" % number.to_i end + + def normalized_versions + pluck(:version).map { |v| normalize_migration_number v } + end end def version diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 3e43591672..fca4f1c9d3 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -11,11 +11,11 @@ module ActiveRecord module ClassMethods def current_scope #:nodoc: - ScopeRegistry.value_for(:current_scope, base_class.to_s) + ScopeRegistry.value_for(:current_scope, self.to_s) end def current_scope=(scope) #:nodoc: - ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope) + ScopeRegistry.set_value_for(:current_scope, self.to_s, scope) end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 49cadb66d0..43c7b1c574 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -30,7 +30,13 @@ module ActiveRecord end def default_scoped # :nodoc: - relation.merge(build_default_scope) + scope = build_default_scope + + if scope + relation.spawn.merge!(scope) + else + relation + end end # Collects attributes from scopes that should be applied when creating @@ -139,6 +145,10 @@ module ActiveRecord # Article.published.featured.latest_article # Article.featured.titles def scope(name, body, &block) + unless body.respond_to?(:call) + raise ArgumentError, 'The scope body needs to be callable.' + end + if dangerous_class_method?(name) raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ "on the model \"#{self.name}\", but Active Record already defined " \ @@ -147,11 +157,20 @@ module ActiveRecord extension = Module.new(&block) if block - singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { body.call(*args) } - scope = scope.extending(extension) if extension + if body.respond_to?(:to_proc) + singleton_class.send(:define_method, name) do |*args| + scope = all.scoping { instance_exec(*args, &body) } + scope = scope.extending(extension) if extension + + scope || all + end + else + singleton_class.send(:define_method, name) do |*args| + scope = all.scoping { body.call(*args) } + scope = scope.extending(extension) if extension - scope || all + scope || all + end end end end diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb new file mode 100644 index 0000000000..0990f815a7 --- /dev/null +++ b/activerecord/lib/active_record/secure_token.rb @@ -0,0 +1,38 @@ +module ActiveRecord + module SecureToken + extend ActiveSupport::Concern + + module ClassMethods + # Example using has_secure_token + # + # # Schema: User(token:string, auth_token:string) + # class User < ActiveRecord::Base + # has_secure_token + # has_secure_token :auth_token + # end + # + # user = User.new + # user.save + # user.token # => "4kUgL2pdQMSCQtjE" + # user.auth_token # => "77TMHrHJFvFDwodq8w7Ev2m7" + # user.regenerate_token # => true + # user.regenerate_auth_token # => true + # + # SecureRandom::base58 is used to generate the 24-character unique token, so collisions are highly unlikely. + # + # Note that it's still possible to generate a race condition in the database in the same way that + # validates_presence_of can. You're encouraged to add a unique index in the database to deal with + # this even more unlikely scenario. + def has_secure_token(attribute = :token) + # Load securerandom only when has_secure_token is used. + require 'active_support/core_ext/securerandom' + define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token } + before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?")} + end + + def generate_unique_secure_token + SecureRandom.base58(24) + end + end + end +end diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index bd9079b596..48c12dcf9f 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -11,7 +11,7 @@ module ActiveRecord #:nodoc: def serializable_hash(options = nil) options = options.try(:clone) || {} - options[:except] = Array(options[:except]).map { |n| n.to_s } + options[:except] = Array(options[:except]).map(&:to_s) options[:except] |= Array(self.class.inheritance_column) super(options) diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 019fe2218e..89b7e0be82 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -180,13 +180,9 @@ module ActiveRecord #:nodoc: class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class - column = klass.columns_hash[name] || Type::Value.new + cast_type = klass.type_for_attribute(name) - type = if column.serialized? - super - else - column.type - end + type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type { :text => :string, :time => :datetime }[type] || type diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index aece446384..95986c820c 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -1,22 +1,33 @@ 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: + # Initializing the cache is done by passing the statement in the create block: # - # cache = ActiveRecord::StatementCache.new do - # Book.where(name: "my book").limit(100) + # cache = StatementCache.create(Book.connection) do |params| + # Book.where(name: "my book").where("author_id > 3") # end # # The cached statement is executed by using the +execute+ method: # - # cache.execute + # cache.execute([], Book, Book.connection) # # 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 - class Substitute; end + # + # If you want to cache the statement without the values you can use the +bind+ method of the + # block parameter. + # + # cache = StatementCache.create(Book.connection) do |params| + # Book.where(name: params.bind) + # end + # + # And pass the bind values as the first argument of +execute+ call. + # + # cache.execute(["my book"], Book, Book.connection) + class StatementCache # :nodoc: + class Substitute; end # :nodoc: - class Query + class Query # :nodoc: def initialize(sql) @sql = sql end @@ -26,7 +37,7 @@ module ActiveRecord end end - class PartialQuery < Query + class PartialQuery < Query # :nodoc: def initialize values @values = values @indexes = values.each_with_index.find_all { |thing,i| @@ -36,8 +47,8 @@ module ActiveRecord def sql_for(binds, connection) val = @values.dup - binds = binds.dup - @indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) } + binds = connection.prepare_binds_for_database(binds) + @indexes.each { |i| val[i] = connection.quote(binds.shift) } val.join end end @@ -51,26 +62,26 @@ module ActiveRecord PartialQuery.new collected end - class Params + class Params # :nodoc: def bind; Substitute.new; end end - class BindMap - def initialize(bind_values) + class BindMap # :nodoc: + def initialize(bound_attributes) @indexes = [] - @bind_values = bind_values + @bound_attributes = bound_attributes - bind_values.each_with_index do |(_, value), i| - if Substitute === value + bound_attributes.each_with_index do |attr, i| + if Substitute === attr.value @indexes << i end end end def bind(values) - bvs = @bind_values.map { |pair| pair.dup } - @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] } - bvs + bas = @bound_attributes.dup + @indexes.each_with_index { |offset,i| bas[offset] = bas[offset].with_cast_value(values[i]) } + bas end end @@ -78,7 +89,7 @@ module ActiveRecord def self.create(connection, block = Proc.new) relation = block.call Params.new - bind_map = BindMap.new relation.bind_values + bind_map = BindMap.new relation.bound_attributes query_builder = connection.cacheable_query relation.arel new query_builder, bind_map end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 7014bc6d45..919bc58ba5 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -15,11 +15,15 @@ module ActiveRecord # You can set custom coder to encode/decode your serialized attributes to/from different formats. # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # - # NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for + # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for # the serialization provided by +store+. Simply use +store_accessor+ instead to generate # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access # using a symbol. # + # NOTE: The default validations with the exception of +uniqueness+ will work. + # For example, if you want to check for +uniqueness+ with +hstore+ you will + # need to use a custom validation to handle it. + # # Examples: # # class User < ActiveRecord::Base @@ -99,7 +103,7 @@ module ActiveRecord self.local_stored_attributes[store_attribute] |= keys end - def _store_accessors_module + def _store_accessors_module # :nodoc: @_store_accessors_module ||= begin mod = Module.new include mod @@ -129,10 +133,10 @@ module ActiveRecord private def store_accessor_for(store_attribute) - @column_types[store_attribute.to_s].accessor + type_for_attribute(store_attribute.to_s).accessor end - class HashAccessor + class HashAccessor # :nodoc: def self.read(object, attribute, key) prepare(object, attribute) object.public_send(attribute)[key] @@ -151,7 +155,7 @@ module ActiveRecord end end - class StringKeyedHashAccessor < HashAccessor + class StringKeyedHashAccessor < HashAccessor # :nodoc: def self.read(object, attribute, key) super object, attribute, key.to_s end @@ -161,7 +165,7 @@ module ActiveRecord end end - class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor + class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc: def self.prepare(object, store_attribute) attribute = object.send(store_attribute) unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb new file mode 100644 index 0000000000..3dd6321a97 --- /dev/null +++ b/activerecord/lib/active_record/table_metadata.rb @@ -0,0 +1,62 @@ +module ActiveRecord + class TableMetadata # :nodoc: + delegate :foreign_type, :foreign_key, to: :association, prefix: true + delegate :association_primary_key, to: :association + + def initialize(klass, arel_table, association = nil) + @klass = klass + @arel_table = arel_table + @association = association + end + + def resolve_column_aliases(hash) + hash = hash.dup + hash.keys.grep(Symbol) do |key| + if klass.attribute_alias? key + hash[klass.attribute_alias(key)] = hash.delete key + end + end + hash + end + + def arel_attribute(column_name) + arel_table[column_name] + end + + def type(column_name) + if klass + klass.type_for_attribute(column_name.to_s) + else + Type::Value.new + end + end + + def associated_with?(association_name) + klass && klass._reflect_on_association(association_name) + end + + def associated_table(table_name) + return self if table_name == arel_table.name + + association = klass._reflect_on_association(table_name) + if association && !association.polymorphic? + association_klass = association.klass + arel_table = association_klass.arel_table + else + type_caster = TypeCaster::Connection.new(klass, table_name) + association_klass = nil + arel_table = Arel::Table.new(table_name, type_caster: type_caster) + end + + TableMetadata.new(association_klass, arel_table, association) + end + + def polymorphic_association? + association && association.polymorphic? + end + + protected + + attr_reader :klass, :arel_table, :association + end +end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 649f316ed8..69aceb66b1 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: @@ -58,7 +60,11 @@ module ActiveRecord end def fixtures_path - @fixtures_path ||= File.join(root, 'test', 'fixtures') + @fixtures_path ||= if ENV['FIXTURES_PATH'] + File.join(root, ENV['FIXTURES_PATH']) + else + File.join(root, 'test', 'fixtures') + end end def root @@ -106,6 +112,8 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).drop + rescue ActiveRecord::NoDatabaseError + $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error $stderr.puts error, *(error.backtrace) $stderr.puts "Couldn't drop #{configuration['database']}" @@ -121,6 +129,18 @@ module ActiveRecord } end + def migrate + verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true + version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil + scope = ENV['SCOPE'] + verbose_was, Migration.verbose = Migration.verbose, verbose + Migrator.migrate(Migrator.migrations_paths, version) do |migration| + scope.blank? || scope == migration.scope + end + ensure + Migration.verbose = verbose_was + end + def charset_current(environment = env) charset ActiveRecord::Base.configurations[environment] end @@ -143,6 +163,19 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(configuration).purge end + def purge_all + each_local_configuration { |configuration| + purge configuration + } + end + + def purge_current(environment = env) + each_current_configuration(environment) { |configuration| + purge configuration + } + ActiveRecord::Base.establish_connection(environment.to_sym) + end + def structure_dump(*arguments) configuration = arguments.first filename = arguments.delete_at 1 @@ -155,23 +188,52 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) end - def load_schema(format = ActiveRecord::Base.schema_format, file = nil) + def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: + file ||= schema_file(format) + case format when :ruby - file ||= File.join(db_dir, "schema.rb") check_schema_file(file) - purge(current_config) + ActiveRecord::Base.establish_connection(configuration) load(file) when :sql - file ||= File.join(db_dir, "structure.sql") check_schema_file(file) - purge(current_config) - structure_load(current_config, file) + structure_load(configuration, file) else raise ArgumentError, "unknown format #{format.inspect}" end end + def load_schema_for(*args) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + This method was renamed to `#load_schema` and will be removed in the future. + Use `#load_schema` instead. + MSG + load_schema(*args) + end + + def schema_file(format = ActiveSupport::Base.schema_format) + case format + when :ruby + File.join(db_dir, "schema.rb") + when :sql + File.join(db_dir, "structure.sql") + end + end + + def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env) + if File.exist?(file || schema_file(format)) + load_schema_current(format, file, environment) + end + end + + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) + each_current_configuration(environment) { |configuration| + load_schema configuration, format, file + } + ActiveRecord::Base.establish_connection(environment.to_sym) + end + def check_schema_file(filename) unless File.exist?(filename) message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index c755831e6d..eafbb2c249 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -31,6 +31,7 @@ module ActiveRecord end establish_connection configuration else + $stderr.puts error.inspect $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding'] end @@ -42,7 +43,7 @@ module ActiveRecord end def purge - establish_connection :test + establish_connection configuration connection.recreate_database configuration['database'], creation_options end @@ -124,7 +125,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; end def root_password - $stdout.print "Please provide the root password for your mysql installation\n>" + $stdout.print "Please provide the root password for your MySQL installation\n>" $stdin.gets.strip end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 3d02ee07d0..ce1de4b76e 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -54,7 +54,7 @@ module ActiveRecord command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" raise 'Error dumping database' unless Kernel.system(command) - File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } + File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end def structure_load(filename) diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index e2e37e7c00..20e4235788 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Timestamp # @@ -48,8 +47,9 @@ module ActiveRecord current_time = current_time_from_proper_timezone all_timestamp_attributes.each do |column| - if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil? - write_attribute(column.to_s, current_time) + column = column.to_s + if has_attribute?(column) && !attribute_present?(column) + write_attribute(column, current_time) end end end @@ -57,8 +57,8 @@ module ActiveRecord super end - def _update_record(*args) - if should_record_timestamps? + def _update_record(*args, touch: true, **options) + if touch && should_record_timestamps? current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each do |column| @@ -67,7 +67,7 @@ module ActiveRecord write_attribute(column, current_time) end end - super + super(*args) end def should_record_timestamps? @@ -114,7 +114,7 @@ module ActiveRecord def clear_timestamp_attributes all_timestamp_attributes_in_model.each do |attribute_name| self[attribute_name] = nil - changed_attributes.delete(attribute_name) + clear_attribute_changes([attribute_name]) end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 7e4dc4c895..dd405c7796 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -2,11 +2,11 @@ module ActiveRecord # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions extend ActiveSupport::Concern + #:nodoc: ACTIONS = [:create, :destroy, :update] included do define_callbacks :commit, :rollback, - terminator: ->(_, result) { result == false }, scope: [:kind, :name] end @@ -233,6 +233,16 @@ module ActiveRecord set_callback(:rollback, :after, *args, &block) end + def raise_in_transactional_callbacks + ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks is deprecated and will be removed without replacement.') + true + end + + def raise_in_transactional_callbacks=(value) + ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks= is deprecated, has no effect and will be removed without replacement.') + value + end + private def set_options_for_callbacks!(args) @@ -247,7 +257,7 @@ module ActiveRecord def assert_valid_transaction_action(actions) if (actions - ACTIONS).any? - raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}" + raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS}" end end end @@ -290,16 +300,16 @@ module ActiveRecord # # Ensure that it is not called if the object was never persisted (failed create), # but call it after the commit of a destroyed object. - def committed! #:nodoc: - run_callbacks :commit if destroyed? || persisted? + def committed!(should_run_callbacks: true) #:nodoc: + _run_commit_callbacks if should_run_callbacks && destroyed? || persisted? ensure force_clear_transaction_record_state end # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record # state should be rolled back to the beginning or just to the last savepoint. - def rolledback!(force_restore_state = false) #:nodoc: - run_callbacks :rollback + def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: + _run_rollback_callbacks if should_run_callbacks ensure restore_transaction_record_state(force_restore_state) clear_transaction_record_state @@ -308,9 +318,12 @@ module ActiveRecord # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks # can be called. def add_to_transaction - if self.class.connection.add_transaction_record(self) - remember_transaction_record_state + if has_transactional_callbacks? + self.class.connection.add_transaction_record(self) + else + set_transaction_state(self.class.connection.transaction_state) end + remember_transaction_record_state end # Executes +method+ within a transaction and captures its return value as a @@ -340,14 +353,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 - 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.reverse_merge!( + new_record: @new_record, + destroyed: @destroyed, + frozen?: frozen?, + ) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - @_start_transaction_state[:frozen?] = frozen? end # Clear the new record state and id of a record. @@ -370,7 +381,10 @@ module ActiveRecord thaw unless restore_state[:frozen?] @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] - write_attribute(self.class.primary_key, restore_state[:id]) + pk = self.class.primary_key + if pk && read_attribute(pk) != restore_state[:id] + write_attribute(pk, restore_state[:id]) + end end end end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index f1384e0bb2..2c0cda69d0 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,8 +1,7 @@ -require 'active_record/type/mutable' -require 'active_record/type/numeric' -require 'active_record/type/time_value' +require 'active_record/type/helpers' require 'active_record/type/value' +require 'active_record/type/big_integer' require 'active_record/type/binary' require 'active_record/type/boolean' require 'active_record/type/date' @@ -15,6 +14,53 @@ require 'active_record/type/serialized' require 'active_record/type/string' require 'active_record/type/text' require 'active_record/type/time' +require 'active_record/type/unsigned_integer' +require 'active_record/type/adapter_specific_registry' require 'active_record/type/type_map' require 'active_record/type/hash_lookup_type_map' + +module ActiveRecord + module Type + @registry = AdapterSpecificRegistry.new + + class << self + attr_accessor :registry # :nodoc: + delegate :add_modifier, to: :registry + + # Add a new type to the registry, allowing it to be referenced as a + # symbol by ActiveRecord::Attributes::ClassMethods#attribute. If your + # type is only meant to be used with a specific database adapter, you can + # do so by passing +adapter: :postgresql+. If your type has the same + # name as a native type for the current adapter, an exception will be + # raised unless you specify an +:override+ option. +override: true+ will + # cause your type to be used instead of the native type. +override: + # false+ will cause the native type to be used over yours if one exists. + def register(type_name, klass = nil, **options, &block) + registry.register(type_name, klass, **options, &block) + end + + def lookup(*args, adapter: current_adapter_name, **kwargs) # :nodoc: + registry.lookup(*args, adapter: adapter, **kwargs) + end + + private + + def current_adapter_name + ActiveRecord::Base.connection.adapter_name.downcase.to_sym + end + end + + register(:big_integer, Type::BigInteger, override: false) + register(:binary, Type::Binary, override: false) + register(:boolean, Type::Boolean, override: false) + register(:date, Type::Date, override: false) + register(:date_time, Type::DateTime, override: false) + register(:decimal, Type::Decimal, override: false) + register(:float, Type::Float, override: false) + register(:integer, Type::Integer, override: false) + register(:string, Type::String, override: false) + register(:text, Type::Text, override: false) + register(:time, Type::Time, override: false) + end +end diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb new file mode 100644 index 0000000000..5f71b3cb94 --- /dev/null +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -0,0 +1,142 @@ +module ActiveRecord + # :stopdoc: + module Type + class AdapterSpecificRegistry + def initialize + @registrations = [] + end + + def register(type_name, klass = nil, **options, &block) + block ||= proc { |_, *args| klass.new(*args) } + registrations << Registration.new(type_name, block, **options) + end + + def lookup(symbol, *args) + registration = registrations + .select { |r| r.matches?(symbol, *args) } + .max + + if registration + registration.call(self, symbol, *args) + else + raise ArgumentError, "Unknown type #{symbol.inspect}" + end + end + + def add_modifier(options, klass, **args) + registrations << DecorationRegistration.new(options, klass, **args) + end + + protected + + attr_reader :registrations + end + + class Registration + def initialize(name, block, adapter: nil, override: nil) + @name = name + @block = block + @adapter = adapter + @override = override + end + + def call(_registry, *args, adapter: nil, **kwargs) + if kwargs.any? # https://bugs.ruby-lang.org/issues/10856 + block.call(*args, **kwargs) + else + block.call(*args) + end + end + + def matches?(type_name, *args, **kwargs) + type_name == name && matches_adapter?(**kwargs) + end + + def <=>(other) + if conflicts_with?(other) + raise TypeConflictError.new("Type #{name} was registered for all + adapters, but shadows a native type with + the same name for #{other.adapter}".squish) + end + priority <=> other.priority + end + + protected + + attr_reader :name, :block, :adapter, :override + + def priority + result = 0 + if adapter + result |= 1 + end + if override + result |= 2 + end + result + end + + def priority_except_adapter + priority & 0b111111100 + end + + private + + def matches_adapter?(adapter: nil, **) + (self.adapter.nil? || adapter == self.adapter) + end + + def conflicts_with?(other) + same_priority_except_adapter?(other) && + has_adapter_conflict?(other) + end + + def same_priority_except_adapter?(other) + priority_except_adapter == other.priority_except_adapter + end + + def has_adapter_conflict?(other) + (override.nil? && other.adapter) || + (adapter && other.override.nil?) + end + end + + class DecorationRegistration < Registration + def initialize(options, klass, adapter: nil) + @options = options + @klass = klass + @adapter = adapter + end + + def call(registry, *args, **kwargs) + subtype = registry.lookup(*args, **kwargs.except(*options.keys)) + klass.new(subtype) + end + + def matches?(*args, **kwargs) + matches_adapter?(**kwargs) && matches_options?(**kwargs) + end + + def priority + super | 4 + end + + protected + + attr_reader :options, :klass + + private + + def matches_options?(**kwargs) + options.all? do |key, value| + kwargs[key] == value + end + end + end + end + + class TypeConflictError < StandardError + end + + # :startdoc: +end diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activerecord/lib/active_record/type/big_integer.rb new file mode 100644 index 0000000000..0c72d8914f --- /dev/null +++ b/activerecord/lib/active_record/type/big_integer.rb @@ -0,0 +1,13 @@ +require 'active_record/type/integer' + +module ActiveRecord + module Type + class BigInteger < Integer # :nodoc: + private + + def max_value + ::Float::INFINITY + end + end + end +end diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb index 3bf29b5026..0baf8c63ad 100644 --- a/activerecord/lib/active_record/type/binary.rb +++ b/activerecord/lib/active_record/type/binary.rb @@ -9,7 +9,7 @@ module ActiveRecord true end - def type_cast(value) + def cast(value) if value.is_a?(Data) value.to_s else @@ -17,23 +17,33 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) return if value.nil? Data.new(super) end - class Data + def changed_in_place?(raw_old_value, value) + old_value = deserialize(raw_old_value) + old_value != value + end + + class Data # :nodoc: def initialize(value) - @value = value + @value = value.to_s end def to_s @value end + alias_method :to_str, :to_s def hex @value.unpack('H*')[0] end + + def ==(other) + other == to_s || super + end end end end diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb index 06dd17ed28..f6a75512fd 100644 --- a/activerecord/lib/active_record/type/boolean.rb +++ b/activerecord/lib/active_record/type/boolean.rb @@ -11,7 +11,7 @@ module ActiveRecord if value == '' nil else - ConnectionAdapters::Column::TRUE_VALUES.include?(value) + !ConnectionAdapters::Column::FALSE_VALUES.include?(value) end end end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb index d90a6069b7..3ceab59ebb 100644 --- a/activerecord/lib/active_record/type/date.rb +++ b/activerecord/lib/active_record/type/date.rb @@ -1,14 +1,12 @@ module ActiveRecord module Type class Date < Value # :nodoc: + include Helpers::AcceptsMultiparameterTime.new + def type :date end - def klass - ::Date - end - def type_cast_for_schema(value) "'#{value.to_s(:db)}'" end @@ -41,6 +39,11 @@ module ActiveRecord ::Date.new(year, mon, mday) rescue nil end end + + def value_from_multiparameter_assignment(*) + time = super + time && time.to_date + end end end end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index 560d63c101..05d2af3808 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -1,12 +1,33 @@ module ActiveRecord module Type class DateTime < Value # :nodoc: - include TimeValue + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 4 => 0, 5 => 0 } + ) def type :datetime end + def serialize(value) + if precision && value.respond_to?(:usec) + number_of_insignificant_digits = 6 - precision + round_power = 10 ** number_of_insignificant_digits + value = value.change(usec: value.usec / round_power * round_power) + end + + if value.acts_like?(:time) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + + if value.respond_to?(zone_conversion_method) + value = value.send(zone_conversion_method) + end + end + + value + end + private def cast_value(string) @@ -28,6 +49,14 @@ module ActiveRecord new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) end + + def value_from_multiparameter_assignment(values_hash) + missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } + if missing_parameter + raise ArgumentError, missing_parameter + end + super + end end end end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index a9db51c6ba..867b5f75c7 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type class Decimal < Value # :nodoc: - include Numeric + include Helpers::Numeric def type :decimal @@ -14,10 +14,33 @@ module ActiveRecord private def cast_value(value) - if value.respond_to?(:to_d) + case value + when ::Float + convert_float_to_big_decimal(value) + when ::Numeric, ::String + BigDecimal(value, precision.to_i) + else + if value.respond_to?(:to_d) + value.to_d + else + cast_value(value.to_s) + end + end + end + + def convert_float_to_big_decimal(value) + if precision + BigDecimal(value, float_precision) + else value.to_d + end + end + + def float_precision + if precision.to_i > ::Float::DIG + 1 + ::Float::DIG + 1 else - value.to_s.to_d + precision.to_i end end end diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb index cabdcecdd7..ff5559e300 100644 --- a/activerecord/lib/active_record/type/decimal_without_scale.rb +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -1,8 +1,8 @@ -require 'active_record/type/integer' +require 'active_record/type/big_integer' module ActiveRecord module Type - class DecimalWithoutScale < Integer # :nodoc: + class DecimalWithoutScale < BigInteger # :nodoc: def type :decimal end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb index 42eb44b9a9..d88482b85d 100644 --- a/activerecord/lib/active_record/type/float.rb +++ b/activerecord/lib/active_record/type/float.rb @@ -1,18 +1,24 @@ module ActiveRecord module Type class Float < Value # :nodoc: - include Numeric + include Helpers::Numeric def type :float end - alias type_cast_for_database type_cast + alias serialize cast private def cast_value(value) - value.to_f + case value + when ::Float then value + when "Infinity" then ::Float::INFINITY + when "-Infinity" then -::Float::INFINITY + when "NaN" then ::Float::NAN + else value.to_f + end end end end diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb index bf92680268..82d9327fc0 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -3,16 +3,14 @@ module ActiveRecord class HashLookupTypeMap < TypeMap # :nodoc: delegate :key?, to: :@mapping - def lookup(type, *args) - @mapping.fetch(type, proc { default_value }).call(type, *args) + def alias_type(type, alias_type) + register_type(type) { |_, *args| lookup(alias_type, *args) } end - def fetch(type, *args, &block) - @mapping.fetch(type, block).call(type, *args) - end + private - def alias_type(type, alias_type) - register_type(type) { |_, *args| lookup(alias_type, *args) } + def perform_fetch(type, *args, &block) + @mapping.fetch(type, block).call(type, *args) end end end diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb new file mode 100644 index 0000000000..634d417d13 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers.rb @@ -0,0 +1,4 @@ +require 'active_record/type/helpers/accepts_multiparameter_time' +require 'active_record/type/helpers/numeric' +require 'active_record/type/helpers/mutable' +require 'active_record/type/helpers/time_value' diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb new file mode 100644 index 0000000000..be571fc1c7 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb @@ -0,0 +1,30 @@ +module ActiveRecord + module Type + module Helpers + class AcceptsMultiparameterTime < Module # :nodoc: + def initialize(defaults: {}) + define_method(:cast) do |value| + if value.is_a?(Hash) + value_from_multiparameter_assignment(value) + else + super(value) + end + end + + define_method(:value_from_multiparameter_assignment) do |values_hash| + defaults.each do |k, v| + values_hash[k] ||= v + end + return unless values_hash[1] && values_hash[2] && values_hash[3] + values = values_hash.sort.map(&:last) + ::Time.send( + ActiveRecord::Base.default_timezone, + *values + ) + end + private :value_from_multiparameter_assignment + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activerecord/lib/active_record/type/helpers/mutable.rb new file mode 100644 index 0000000000..88a9099277 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/mutable.rb @@ -0,0 +1,18 @@ +module ActiveRecord + module Type + module Helpers + module Mutable # :nodoc: + def cast(value) + deserialize(serialize(value)) + end + + # +raw_old_value+ will be the `_before_type_cast` version of the + # value (likely a string). +new_value+ will be the current, type + # cast value. + def changed_in_place?(raw_old_value, new_value) + raw_old_value != serialize(new_value) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activerecord/lib/active_record/type/helpers/numeric.rb new file mode 100644 index 0000000000..a755a02a59 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/numeric.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module Type + module Helpers + module Numeric # :nodoc: + def cast(value) + value = case value + when true then 1 + when false then 0 + when ::String then value.presence + else value + end + super(value) + end + + def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: + super || number_to_non_number?(old_value, new_value_before_type_cast) + end + + private + + def number_to_non_number?(old_value, new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast) + end + + def non_numeric_string?(value) + # 'wibble'.to_i will give zero, we want to make sure + # that we aren't marking int zero to string zero as + # changed. + value.to_s !~ /\A-?\d+\.?\d*\z/ + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activerecord/lib/active_record/type/helpers/time_value.rb new file mode 100644 index 0000000000..6e14c3a9b5 --- /dev/null +++ b/activerecord/lib/active_record/type/helpers/time_value.rb @@ -0,0 +1,40 @@ +module ActiveRecord + module Type + module Helpers + module TimeValue # :nodoc: + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + def user_input_in_time_zone(value) + value.in_time_zone + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return unless time + + time -= offset + Base.default_timezone == :utc ? time : time.getlocal + else + ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb index 08477d1303..2a1b04ac7f 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activerecord/lib/active_record/type/integer.rb @@ -1,13 +1,37 @@ module ActiveRecord module Type class Integer < Value # :nodoc: - include Numeric + include Helpers::Numeric + + # Column storage size in bytes. + # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc. + DEFAULT_LIMIT = 4 + + def initialize(*) + super + @range = min_value...max_value + end def type :integer end - alias type_cast_for_database type_cast + def deserialize(value) + return if value.nil? + value.to_i + end + + def serialize(value) + result = cast(value) + if result + ensure_in_range(result) + end + result + end + + protected + + attr_reader :range private @@ -15,9 +39,25 @@ module ActiveRecord case value when true then 1 when false then 0 - else value.to_i rescue nil + else + value.to_i rescue nil + end + end + + def ensure_in_range(value) + unless range.cover?(value) + raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || DEFAULT_LIMIT}" end end + + def max_value + limit = self.limit || DEFAULT_LIMIT + 1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign + end + + def min_value + -max_value + end end end end diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb deleted file mode 100644 index 64cf4b9b93..0000000000 --- a/activerecord/lib/active_record/type/mutable.rb +++ /dev/null @@ -1,16 +0,0 @@ -module ActiveRecord - module Type - module Mutable - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) - end - - # +raw_old_value+ will be the `_before_type_cast` version of the - # value (likely a string). +new_value+ will be the current, type - # cast value. - def changed_in_place?(raw_old_value, new_value) # :nodoc: - raw_old_value != type_cast_for_database(new_value) - end - end - end -end diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb deleted file mode 100644 index 137c9e4c99..0000000000 --- a/activerecord/lib/active_record/type/numeric.rb +++ /dev/null @@ -1,42 +0,0 @@ -module ActiveRecord - module Type - module Numeric # :nodoc: - def number? - true - end - - def type_cast(value) - value = case value - when true then 1 - when false then 0 - when ::String then value.presence - else value - end - super(value) - end - - def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: - # 0 => 'wibble' should mark as changed so numericality validations run - if nil_or_zero?(old_value) && non_numeric_string?(new_value_before_type_cast) - # nil => '' should not mark as changed - old_value != new_value_before_type_cast.presence - else - super - end - end - - private - - def non_numeric_string?(value) - # 'wibble'.to_i will give zero, we want to make sure - # that we aren't marking int zero to string zero as - # changed. - value !~ /\A\d+\.?\d*\z/ - end - - def nil_or_zero?(value) - value.nil? || value == 0 - end - end - end -end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index ebde14634c..732029c723 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type - class Serialized < SimpleDelegator # :nodoc: - include Mutable + class Serialized < DelegateClass(Type::Value) # :nodoc: + include Helpers::Mutable attr_reader :subtype, :coder @@ -11,43 +11,33 @@ module ActiveRecord super(subtype) end - def type_cast_from_database(value) - if is_default_value?(value) + def deserialize(value) + if default_value?(value) value else coder.load(super) end end - def type_cast_for_database(value) + def serialize(value) return if value.nil? - unless is_default_value?(value) + unless default_value?(value) super coder.dump(value) end end - def serialized? - true + def changed_in_place?(raw_old_value, value) + return false if value.nil? + subtype.changed_in_place?(raw_old_value, serialize(value)) end def accessor ActiveRecord::Store::IndifferentHashAccessor end - def init_with(coder) - @subtype = coder['subtype'] - @coder = coder['coder'] - __setobj__(@subtype) - end - - def encode_with(coder) - coder['subtype'] = @subtype - coder['coder'] = @coder - end - private - def is_default_value?(value) + def default_value?(value) value == coder.load(nil) end end diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index 3b1554bd5a..2662b7e874 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -5,17 +5,30 @@ module ActiveRecord :string end - def text? - true + def changed_in_place?(raw_old_value, new_value) + if new_value.is_a?(::String) + raw_old_value != new_value + end + end + + def serialize(value) + case value + when ::Numeric, ActiveSupport::Duration then value.to_s + when ::String then ::String.new(value) + when true then "t" + when false then "f" + else super + end end private def cast_value(value) case value - when true then "1" - when false then "0" - else value.to_s + when true then "t" + when false then "f" + # String.new is slightly faster than dup + else ::String.new(value.to_s) end end end diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb index 41f7d97f0c..19a10021bc 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -1,12 +1,28 @@ module ActiveRecord module Type class Time < Value # :nodoc: - include TimeValue + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } + ) def type :time end + def user_input_in_time_zone(value) + return unless value.present? + + case value + when ::String + value = "2000-01-01 #{value}" + when ::Time + value = value.change(year: 2000, day: 1, month: 1) + end + + super(value) + end + private def cast_value(value) diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb deleted file mode 100644 index d611d72dd4..0000000000 --- a/activerecord/lib/active_record/type/time_value.rb +++ /dev/null @@ -1,38 +0,0 @@ -module ActiveRecord - module Type - module TimeValue # :nodoc: - def klass - ::Time - end - - def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" - end - - private - - def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) - # Treat 0000-00-00 00:00:00 as nil. - return if year.nil? || (year == 0 && mon == 0 && mday == 0) - - if offset - time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil - return unless time - - time -= offset - Base.default_timezone == :utc ? time : time.getlocal - else - ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index 88c5f9c497..09f5ba6b74 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,24 +1,28 @@ +require 'thread_safe' + module ActiveRecord module Type class TypeMap # :nodoc: def initialize @mapping = {} + @cache = ThreadSafe::Cache.new do |h, key| + h.fetch_or_store(key, ThreadSafe::Cache.new) + end end def lookup(lookup_key, *args) - matching_pair = @mapping.reverse_each.detect do |key, _| - key === lookup_key - end + fetch(lookup_key, *args) { default_value } + end - if matching_pair - matching_pair.last.call(lookup_key, *args) - else - default_value + def fetch(lookup_key, *args, &block) + @cache[lookup_key].fetch_or_store(args) do + perform_fetch(lookup_key, *args, &block) end end def register_type(key, value = nil, &block) raise ::ArgumentError unless value || block + @cache.clear if block @mapping[key] = block @@ -40,6 +44,18 @@ module ActiveRecord private + def perform_fetch(lookup_key, *args) + matching_pair = @mapping.reverse_each.detect do |key, _| + key === lookup_key + end + + if matching_pair + matching_pair.last.call(lookup_key, *args) + else + yield lookup_key, *args + end + end + def default_value @default_value ||= Value.new end diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb new file mode 100644 index 0000000000..ed3e527483 --- /dev/null +++ b/activerecord/lib/active_record/type/unsigned_integer.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Type + class UnsignedInteger < Integer # :nodoc: + private + + def max_value + super * 2 + end + + def min_value + 0 + end + end + end +end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index b34d1697cd..fc3ef5e83b 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -1,80 +1,102 @@ module ActiveRecord module Type - class Value # :nodoc: + class Value attr_reader :precision, :scale, :limit - # Valid options are +precision+, +scale+, and +limit+. - # They are only used when dumping schema. - def initialize(options = {}) - options.assert_valid_keys(:precision, :scale, :limit) - @precision = options[:precision] - @scale = options[:scale] - @limit = options[:limit] + def initialize(precision: nil, limit: nil, scale: nil) + @precision = precision + @scale = scale + @limit = limit end - # The simplified type that this object represents. Subclasses - # must override this method. - def type; end + def type # :nodoc: + end - def type_cast_from_database(value) - type_cast(value) + # Convert a value from database input to the appropriate ruby type. The + # return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. The default + # implementation just calls Value#cast. + # + # +value+ The raw input, as provided from the database. + def deserialize(value) + cast(value) end - def type_cast_from_user(value) - type_cast(value) + # Type casts a value from user input (e.g. from a setter). This value may + # be a string from the form builder, or a ruby object passed to a setter. + # There is currently no way to differentiate between which source it came + # from. + # + # The return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. See also: + # Value#cast_value. + # + # +value+ The raw input, as provided to the attribute setter. + def cast(value) + cast_value(value) unless value.nil? end - def type_cast_for_database(value) + # Cast a value from the ruby type to a type that the database knows how + # to understand. The returned value from this method should be a + # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or + # +nil+. + def serialize(value) value end - def type_cast_for_schema(value) + # Type cast a value for schema dumping. This method is private, as we are + # hoping to remove it entirely. + def type_cast_for_schema(value) # :nodoc: value.inspect end - def text? - false - end - - def number? + # These predicates are not documented, as I need to look further into + # their use, and see if they can be removed entirely. + def binary? # :nodoc: false end - def binary? - false + # Determines whether a value has changed for dirty checking. +old_value+ + # and +new_value+ will always be type-cast. Types should not need to + # override this method. + def changed?(old_value, new_value, _new_value_before_type_cast) + old_value != new_value end - def serialized? + # Determines whether the mutable value has been modified since it was + # read. Returns +false+ by default. If your type returns an object + # which could be mutated, you should override this method. You will need + # to either: + # + # - pass +new_value+ to Value#serialize and compare it to + # +raw_old_value+ + # + # or + # + # - pass +raw_old_value+ to Value#deserialize and compare it to + # +new_value+ + # + # +raw_old_value+ The original value, before being passed to + # +deserialize+. + # + # +new_value+ The current value, after type casting. + def changed_in_place?(raw_old_value, new_value) false end - def klass # :nodoc: - end - - # +old_value+ will always be type-cast. - # +new_value+ will come straight from the database - # or from assignment, so it could be anything. Types - # which cannot typecast arbitrary values should override - # this method. - def changed?(old_value, new_value, _new_value_before_type_cast) # :nodoc: - old_value != new_value - end - - def changed_in_place?(*) # :nodoc: - false + def ==(other) + self.class == other.class && + precision == other.precision && + scale == other.scale && + limit == other.limit end private - # Takes an input from the database, or from attribute setters, - # and casts it to a type appropriate for this object. This method - # should not be overriden by subclasses. Instead, override `cast_value`. - def type_cast(value) # :api: public - cast_value(value) unless value.nil? - end - # Responsible for casting values from external sources to the appropriate - # type. Called by `type_cast` for all values except `nil`. - def cast_value(value) # :api: public + # Convenience method for types which do not need separate type casting + # behavior for user and database inputs. Called by Value#cast for + # values except +nil+. + def cast_value(value) # :doc: value end end diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb new file mode 100644 index 0000000000..63ba10c289 --- /dev/null +++ b/activerecord/lib/active_record/type_caster.rb @@ -0,0 +1,7 @@ +require 'active_record/type_caster/map' +require 'active_record/type_caster/connection' + +module ActiveRecord + module TypeCaster + end +end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb new file mode 100644 index 0000000000..3878270770 --- /dev/null +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -0,0 +1,29 @@ +module ActiveRecord + module TypeCaster + class Connection + def initialize(klass, table_name) + @klass = klass + @table_name = table_name + end + + def type_cast_for_database(attribute_name, value) + return value if value.is_a?(Arel::Nodes::BindParam) + column = column_for(attribute_name) + connection.type_cast_from_column(column, value) + end + + protected + + attr_reader :table_name + delegate :connection, to: :@klass + + private + + def column_for(attribute_name) + if connection.schema_cache.table_exists?(table_name) + connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] + end + end + end + end +end diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb new file mode 100644 index 0000000000..4b1941351c --- /dev/null +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module TypeCaster + class Map + def initialize(types) + @types = types + end + + def type_cast_for_database(attr_name, value) + return value if value.is_a?(Arel::Nodes::BindParam) + type = types.type_for_attribute(attr_name.to_s) + type.serialize(value) + end + + protected + + attr_reader :types + end + end +end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 9999624fcf..f27adc9c40 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -5,13 +5,14 @@ module ActiveRecord # +record+ method to retrieve the record which did not validate. # # begin - # complex_operation_that_calls_save!_internally + # complex_operation_that_internally_calls_save! # rescue ActiveRecord::RecordInvalid => invalid # puts invalid.record.errors # end class RecordInvalid < ActiveRecordError - attr_reader :record # :nodoc: - def initialize(record) # :nodoc: + attr_reader :record + + def initialize(record) @record = record errors = @record.errors.full_messages.join(", ") super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid")) @@ -29,21 +30,6 @@ module ActiveRecord extend ActiveSupport::Concern include ActiveModel::Validations - module ClassMethods - # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+ - # so an exception is raised if the record is invalid. - def create!(attributes = nil, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create!(attr, &block) } - else - object = new(attributes) - yield(object) if block_given? - object.save! - object - end - end - end - # The validation process on save can be skipped by passing <tt>validate: false</tt>. # The regular Base#save method is replaced with this when the validations # module is mixed in, which it is by default. @@ -54,7 +40,7 @@ module ActiveRecord # Attempts to save the record just like Base#save but will raise a +RecordInvalid+ # exception instead of returning +false+ if the record is not valid. def save!(options={}) - perform_validations(options) ? super : raise(RecordInvalid.new(self)) + perform_validations(options) ? super : raise_record_invalid end # Runs all the validations within the specified context. Returns +true+ if @@ -75,8 +61,24 @@ module ActiveRecord alias_method :validate, :valid? + # Runs all the validations within the specified context. Returns +true+ if + # no errors are found, raises +RecordInvalid+ otherwise. + # + # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if + # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. + # + # Validations with no <tt>:on</tt> option will run no matter the context. Validations with + # some <tt>:on</tt> option will only run in the specified context. + def validate!(context = nil) + valid?(context) || raise_record_invalid + end + protected + def raise_record_invalid + raise(RecordInvalid.new(self)) + end + def perform_validations(options={}) # :nodoc: options[:validate] == false || valid?(options[:context]) end @@ -86,3 +88,4 @@ end require "active_record/validations/associated" require "active_record/validations/uniqueness" require "active_record/validations/presence" +require "active_record/validations/length" diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index b4785d3ba4..47ccef31a5 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -29,9 +29,11 @@ module ActiveRecord # Configuration options: # # * <tt>:message</tt> - A custom error message (default is: "is invalid"). - # * <tt>:on</tt> - Specifies when this validation is active. Runs in all - # validation contexts by default (+nil+), other options are <tt>:create</tt> - # and <tt>:update</tt>. + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:if</tt> - Specifies a method, proc or string to call to determine # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb new file mode 100644 index 0000000000..5991fbad8e --- /dev/null +++ b/activerecord/lib/active_record/validations/length.rb @@ -0,0 +1,33 @@ +module ActiveRecord + module Validations + class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc: + def validate_each(record, attribute, association_or_value) + return unless should_validate?(record) || associations_are_dirty?(record) + if association_or_value.respond_to?(:loaded?) && association_or_value.loaded? + association_or_value = association_or_value.target.reject(&:marked_for_destruction?) + end + super + end + + def associations_are_dirty?(record) + attributes.any? do |attribute| + value = record.read_attribute_for_validation(attribute) + if value.respond_to?(:loaded?) && value.loaded? + value.target.any?(&:marked_for_destruction?) + else + false + end + end + end + end + + module ClassMethods + # See <tt>ActiveModel::Validation::LengthValidator</tt> for more information. + def validates_length_of(*attr_names) + validates_with LengthValidator, _merge_attributes(attr_names) + end + + alias_method :validates_size_of, :validates_length_of + end + end +end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index e586744818..75d5bd5a35 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -2,13 +2,14 @@ module ActiveRecord module Validations class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc: def validate(record) + return unless should_validate?(record) super attributes.each do |attribute| next unless record.class._reflect_on_association(attribute) associated_records = Array.wrap(record.send(attribute)) # Superclass validates presence. Ensure present records aren't about to be destroyed. - if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? } + if associated_records.present? && associated_records.all?(&:marked_for_destruction?) record.errors.add(attribute, :blank, options) end end @@ -44,9 +45,11 @@ module ActiveRecord # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). - # * <tt>:on</tt> - Specifies when this validation is active. Runs in all - # validation contexts by default (+nil+), other options are <tt>:create</tt> - # and <tt>:update</tt>. + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if # the validation should occur (e.g. <tt>if: :allow_validation</tt>, or # <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 2e7b1d7206..9be4b10a55 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -11,14 +11,14 @@ module ActiveRecord end def validate_each(record, attribute, value) + return unless should_validate?(record) finder_class = find_finder_class_for(record) table = finder_class.arel_table value = map_enum_attribute(finder_class, attribute, value) relation = build_relation(finder_class, table, attribute, value) - relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? + relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted? relation = scope_relation(record, table, relation) - relation = finder_class.unscoped.where(relation) relation = relation.merge(options[:conditions]) if options[:conditions] if relation.exists? @@ -48,7 +48,7 @@ module ActiveRecord def build_relation(klass, table, attribute, value) #:nodoc: if reflection = klass._reflect_on_association(attribute) attribute = reflection.foreign_key - value = value.attributes[reflection.primary_key_column.name] unless value.nil? + value = value.attributes[reflection.klass.primary_key] unless value.nil? end attribute_name = attribute.to_s @@ -60,15 +60,22 @@ module ActiveRecord end column = klass.columns_hash[attribute_name] - value = klass.connection.type_cast(value, column) - value = value.to_s[0, column.limit] if value && column.limit && column.text? + cast_type = klass.type_for_attribute(attribute_name) + value = cast_type.serialize(value) + value = klass.connection.type_cast(value) + if value.is_a?(String) && column.limit + value = value.to_s[0, column.limit] + end + + value = Arel::Nodes::Quoted.new(value) - if !options[:case_sensitive] && value && column.text? + comparison = if !options[:case_sensitive] && !value.nil? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else klass.connection.case_sensitive_comparison(table, attribute, column, value) end + klass.unscoped.where(comparison) end def scope_relation(record, table, relation) @@ -77,9 +84,9 @@ module ActiveRecord scope_value = record.send(reflection.foreign_key) scope_item = reflection.foreign_key else - scope_value = record.read_attribute(scope_item) + scope_value = record._read_attribute(scope_item) end - relation = relation.and(table[scope_item].eq(scope_value)) + relation = relation.where(scope_item => scope_value) end relation @@ -150,7 +157,7 @@ module ActiveRecord # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, # proc or string should return or evaluate to a +true+ or +false+ value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to - # determine if the validation should ot occur (e.g. <tt>unless: :skip_validation</tt>, + # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>, # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a +true+ or +false+ # value. |