diff options
Diffstat (limited to 'activerecord/lib')
229 files changed, 7713 insertions, 1843 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 5de6503144..d198466dbf 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -40,6 +40,7 @@ module ActiveRecord autoload :Core autoload :ConnectionHandling autoload :CounterCache + autoload :DatabaseConfigurations autoload :DynamicMatchers autoload :Enum autoload :InternalMetadata @@ -163,6 +164,7 @@ module ActiveRecord "active_record/tasks/postgresql_database_tasks" end + autoload :TestDatabases, "active_record/test_databases" autoload :TestFixtures, "active_record/fixtures" def self.eager_load! diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index e5e89734d2..3250e29b82 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -3,8 +3,6 @@ module ActiveRecord # See ActiveRecord::Aggregations::ClassMethods for documentation module Aggregations - extend ActiveSupport::Concern - def initialize_dup(*) # :nodoc: @aggregation_cache = {} super @@ -35,7 +33,7 @@ module ActiveRecord # the database). # # class Customer < ActiveRecord::Base - # composed_of :balance, class_name: "Money", mapping: %w(amount currency) + # composed_of :balance, class_name: "Money", mapping: %w(balance amount) # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # end # @@ -177,9 +175,9 @@ module ActiveRecord # # Once a #composed_of relationship is specified for a model, records can be loaded from the database # by specifying an instance of the value object in the conditions hash. The following example - # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": + # finds all customers with +address_street+ equal to "May Street" and +address_city+ equal to "Chicago": # - # Customer.where(balance: Money.new(20, "USD")) + # Customer.where(address: Address.new("May Street", "Chicago")) # module ClassMethods # Adds reader and writer methods for manipulating a value object: @@ -212,8 +210,7 @@ module ActiveRecord # # Option examples: # composed_of :temperature, mapping: %w(reading celsius) - # composed_of :balance, class_name: "Money", mapping: %w(balance amount), - # converter: Proc.new { |balance| balance.to_money } + # composed_of :balance, class_name: "Money", mapping: %w(balance amount) # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # composed_of :gps_location # composed_of :gps_location, allow_nil: true @@ -226,6 +223,10 @@ module ActiveRecord def composed_of(part_id, options = {}) options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) + unless self < Aggregations + include Aggregations + end + name = part_id.id2name class_name = options[:class_name] || name.camelize mapping = options[:mapping] || [ name, name ] diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 2b0b2864bc..403667fb70 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -2,8 +2,8 @@ module ActiveRecord class AssociationRelation < Relation - def initialize(klass, table, predicate_builder, association) - super(klass, table, predicate_builder) + def initialize(klass, association) + super(klass) @association = association end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 661605d3e5..1ee52945ea 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -241,7 +241,7 @@ module ActiveRecord association end - def association_cached?(name) # :nodoc + def association_cached?(name) # :nodoc: @association_cache.key?(name) end @@ -292,13 +292,13 @@ module ActiveRecord # # The project class now has the following methods (and more) to ease the traversal and # manipulation of its relationships: - # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> - # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> - # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> - # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt> - # <tt>Project#milestones.build, Project#milestones.create</tt> - # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> - # <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt> + # * <tt>Project#portfolio</tt>, <tt>Project#portfolio=(portfolio)</tt>, <tt>Project#reload_portfolio</tt> + # * <tt>Project#project_manager</tt>, <tt>Project#project_manager=(project_manager)</tt>, <tt>Project#reload_project_manager</tt> + # * <tt>Project#milestones.empty?</tt>, <tt>Project#milestones.size</tt>, <tt>Project#milestones</tt>, <tt>Project#milestones<<(milestone)</tt>, + # <tt>Project#milestones.delete(milestone)</tt>, <tt>Project#milestones.destroy(milestone)</tt>, <tt>Project#milestones.find(milestone_id)</tt>, + # <tt>Project#milestones.build</tt>, <tt>Project#milestones.create</tt> + # * <tt>Project#categories.empty?</tt>, <tt>Project#categories.size</tt>, <tt>Project#categories</tt>, <tt>Project#categories<<(category1)</tt>, + # <tt>Project#categories.delete(category1)</tt>, <tt>Project#categories.destroy(category1)</tt> # # === A word of warning # @@ -1061,12 +1061,6 @@ module ActiveRecord # belongs_to :dungeon, inverse_of: :evil_wizard # end # - # There are limitations to <tt>:inverse_of</tt> support: - # - # * does not work with <tt>:through</tt> associations. - # * does not work with <tt>:polymorphic</tt> associations. - # * inverse associations for #belongs_to associations #has_many are ignored. - # # For more information, see the documentation for the +:inverse_of+ option. # # == Deleting from associations @@ -1238,9 +1232,9 @@ module ActiveRecord # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>) # * <tt>Firm#clients.find</tt> (similar to <tt>Client.where(firm_id: id).find(id)</tt>) # * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>) - # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) - # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) - # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>) + # * <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>) # * <tt>Firm#clients.reload</tt> # The declaration can also include an +options+ hash to specialize the behavior of the association. # @@ -1279,6 +1273,9 @@ 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>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [: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 @@ -1352,8 +1349,7 @@ module ActiveRecord # <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] # Specifies the name of the #belongs_to association on the associated object - # that is the inverse of this #has_many association. Does not work in combination - # with <tt>:through</tt> or <tt>:as</tt> options. + # that is the inverse of this #has_many association. # 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. @@ -1409,9 +1405,9 @@ module ActiveRecord # An Account class declares <tt>has_one :beneficiary</tt>, which will add: # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</tt>) # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>) - # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>) - # * <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>) + # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new(account_id: id)</tt>) + # * <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>) # * <tt>Account#reload_beneficiary</tt> # # === Scopes @@ -1449,6 +1445,9 @@ 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_one association # will use "person_id" as the default <tt>:foreign_key</tt>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [: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 @@ -1464,6 +1463,9 @@ module ActiveRecord # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the # source reflection. You can only use a <tt>:through</tt> query through a #has_one # or #belongs_to association on the join model. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:source] # Specifies the source association name used by #has_one <tt>:through</tt> queries. # Only use it if the name cannot be inferred from the association. @@ -1484,8 +1486,7 @@ module ActiveRecord # <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] # Specifies the name of the #belongs_to association on the associated object - # that is the inverse of this #has_one association. Does not work in combination - # with <tt>:through</tt> or <tt>:as</tt> options. + # that is the inverse of this #has_one association. # 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. @@ -1570,6 +1571,9 @@ module ActiveRecord # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, # <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key # of "favorite_person_id". + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [: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 association with a "_type" @@ -1619,8 +1623,7 @@ module ActiveRecord # +after_commit+ and +after_rollback+ callbacks are executed. # [:inverse_of] # Specifies the name of the #has_one or #has_many association on the associated - # object that is the inverse of this #belongs_to association. Does not work in - # combination with the <tt>:polymorphic</tt> options. + # object that is the inverse of this #belongs_to association. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:optional] # When set to +true+, the association will not have its presence validated. @@ -1743,8 +1746,8 @@ module ActiveRecord # * <tt>Developer#projects.size</tt> # * <tt>Developer#projects.find(id)</tt> # * <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>) + # * <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>) # * <tt>Developer#projects.reload</tt> # The declaration may include an +options+ hash to specialize the behavior of the association. # @@ -1789,6 +1792,9 @@ module ActiveRecord # of this class in lower-case and "_id" suffixed. So a Person class that makes # a #has_and_belongs_to_many association to Project will use "person_id" as the # default <tt>:foreign_key</tt>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:association_foreign_key] # Specify the foreign key used for the association on the receiving side of the association. # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 14881cfe17..272eede824 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -30,20 +30,12 @@ module ActiveRecord join.left.scan( /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i ).size - elsif join.respond_to? :left + elsif join.is_a?(Arel::Nodes::Join) join.left.name == name ? 1 : 0 elsif join.is_a?(Hash) - join.fetch(name, 0) + join[name] else - # this branch is reached by two tests: - # - # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37 - # with :posts - # - # activerecord/test/cases/associations/eager_test.rb:1133 - # with :comments - # - 0 + raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join" end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index ca1f9f1650..44596f4424 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -19,7 +19,6 @@ module ActiveRecord # HasManyThroughAssociation + ThroughAssociation class Association #:nodoc: attr_reader :owner, :target, :reflection - attr_accessor :inversed delegate :options, to: :reflection @@ -67,7 +66,7 @@ module ActiveRecord # # Note that if the target has not been loaded, it is not considered stale. def stale_target? - !inversed && loaded? && @stale_state != stale_state + !@inversed && loaded? && @stale_state != stale_state end # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+. @@ -98,23 +97,24 @@ module ActiveRecord # Set the inverse association, if possible def set_inverse_instance(record) - if invertible_for?(record) - inverse = record.association(inverse_reflection_for(record).name) - inverse.target = owner - inverse.inversed = true + if inverse = inverse_association_for(record) + inverse.inversed_from(owner) end record end # Remove the inverse association, if possible def remove_inverse_instance(record) - if invertible_for?(record) - inverse = record.association(inverse_reflection_for(record).name) - inverse.target = nil - inverse.inversed = false + if inverse = inverse_association_for(record) + inverse.inversed_from(nil) end end + def inversed_from(record) + self.target = record + @inversed = !!record + end + # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. def klass @@ -124,7 +124,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, klass.predicate_builder, self).merge!(klass.all) + AssociationRelation.create(klass, self).merge!(klass.all) end def extensions @@ -156,9 +156,9 @@ module ActiveRecord reset end - # We can't dump @reflection since it contains the scope proc + # We can't dump @reflection and @through_reflection since it contains the scope proc def marshal_dump - ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] } + ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] } [@reflection.name, ivars] end @@ -201,8 +201,8 @@ module ActiveRecord if (reflection.has_one? || reflection.collection?) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] - if reflection.options[:as] - attributes[reflection.type] = owner.class.base_class.name + if reflection.type + attributes[reflection.type] = owner.class.polymorphic_name end end @@ -240,6 +240,12 @@ module ActiveRecord end end + def inverse_association_for(record) + if invertible_for?(record) + record.association(inverse_reflection_for(record).name) + end + end + # Can be redefined by subclasses, notably polymorphic belongs_to # The record parameter is necessary to support polymorphic inverses as we must check for # the association in the specific class of the record. @@ -269,6 +275,7 @@ module ActiveRecord def build_record(attributes) reflection.build_association(attributes) do |record| initialize_attributes(record, attributes) + yield(record) if block_given? end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 11967e0571..0a90a6104a 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -35,24 +35,20 @@ module ActiveRecord binds << last_reflection.join_id_for(owner) if last_reflection.type - binds << owner.class.base_class.name + binds << owner.class.polymorphic_name end chain.each_cons(2).each do |reflection, next_reflection| if reflection.type - binds << next_reflection.klass.base_class.name + binds << next_reflection.klass.polymorphic_name end end binds end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :value_transformation - private def join(table, constraint) table.create_join(table, table.create_on(constraint)) end @@ -67,7 +63,7 @@ module ActiveRecord scope = apply_scope(scope, table, key, value) if reflection.type - polymorphic_type = transform_value(owner.class.base_class.name) + polymorphic_type = transform_value(owner.class.polymorphic_name) scope = apply_scope(scope, table, reflection.type, polymorphic_type) end @@ -88,7 +84,7 @@ module ActiveRecord constraint = table[key].eq(foreign_table[foreign_key]) if reflection.type - value = transform_value(next_reflection.klass.base_class.name) + value = transform_value(next_reflection.klass.polymorphic_name) scope = apply_scope(scope, table, reflection.type, value) end @@ -135,7 +131,7 @@ module ActiveRecord item = eval_scope(reflection, scope_chain_item, owner) if scope_chain_item == chain_head.scope - scope.merge! item.except(:where, :includes) + scope.merge! item.except(:where, :includes, :unscope, :order) end reflection.all_includes do @@ -144,7 +140,7 @@ module ActiveRecord scope.unscope!(*item.unscope_values) scope.where_clause += item.where_clause - scope.order_values |= item.order_values + scope.order_values = item.order_values | scope.order_values 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 ba54cd8f49..3d4ad1dd5b 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -5,22 +5,20 @@ module ActiveRecord # = Active Record Belongs To Association class BelongsToAssociation < SingularAssociation #:nodoc: def handle_dependency - target.send(options[:dependent]) if load_target - end + return unless load_target - def replace(record) - if record - raise_on_type_mismatch!(record) - update_counters_on_replace(record) - replace_keys(record) - set_inverse_instance(record) - @updated = true + case options[:dependent] + when :destroy + target.destroy + raise ActiveRecord::Rollback unless target.destroyed? else - decrement_counters - remove_keys + target.send(options[:dependent]) end + end - self.target = record + def inversed_from(record) + replace_keys(record) + super end def default(&block) @@ -44,14 +42,32 @@ module ActiveRecord update_counters(1) end + def target_changed? + owner.saved_change_to_attribute?(reflection.foreign_key) + end + private + def replace(record) + if record + raise_on_type_mismatch!(record) + update_counters_on_replace(record) + set_inverse_instance(record) + @updated = true + else + decrement_counters + end + + replace_keys(record) + + self.target = record + end def update_counters(by) if require_counter_update? && foreign_key_present? if target && !stale_target? - target.increment!(reflection.counter_cache_column, by) + target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch]) else - klass.update_counters(target_id, reflection.counter_cache_column => by) + counter_cache_target.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch]) end end end @@ -67,22 +83,22 @@ module ActiveRecord def update_counters_on_replace(record) if require_counter_update? && different_target?(record) owner.instance_variable_set :@_after_replace_counter_called, true - record.increment!(reflection.counter_cache_column) + record.increment!(reflection.counter_cache_column, touch: reflection.options[:touch]) decrement_counters end end # Checks whether record is different to the current target, without loading it def different_target?(record) - record.id != owner._read_attribute(reflection.foreign_key) + record._read_attribute(primary_key(record)) != owner._read_attribute(reflection.foreign_key) end def replace_keys(record) - owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class)) + owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record)) : nil end - def remove_keys - owner[reflection.foreign_key] = nil + def primary_key(record) + reflection.association_primary_key(record.class) end def foreign_key_present? @@ -96,12 +112,9 @@ module ActiveRecord inverse && inverse.has_one? end - def target_id - if options[:primary_key] - owner.send(reflection.name).try(:id) - else - owner._read_attribute(reflection.foreign_key) - end + def counter_cache_target + primary_key = reflection.association_primary_key(klass) + klass.unscoped.where!(primary_key => owner._read_attribute(reflection.foreign_key)) end def stale_state diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 4ce3474bd5..3fd2fb5f67 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -9,16 +9,14 @@ module ActiveRecord type.presence && type.constantize end - private + def target_changed? + super || owner.saved_change_to_attribute?(reflection.foreign_type) + end + private def replace_keys(record) super - owner[reflection.foreign_type] = record.class.base_class.name - end - - def remove_keys - super - owner[reflection.foreign_type] = nil + owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil end def different_target?(record) diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index ca3032d967..7c69cd65ee 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -104,8 +104,8 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.define_readers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}(*args) - association(:#{name}).reader(*args) + def #{name} + association(:#{name}).reader end CODE end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 9904ee4bed..0166ed98ca 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -34,9 +34,9 @@ module ActiveRecord::Associations::Builder # :nodoc: foreign_key = reflection.foreign_key cache_column = reflection.counter_cache_column - if (@_after_replace_counter_called ||= false) + if @_after_replace_counter_called ||= false @_after_replace_counter_called = false - elsif saved_change_to_attribute?(foreign_key) && !new_record? + elsif association(reflection.name).target_changed? if reflection.polymorphic? model = attribute_in_database(reflection.foreign_type).try(:constantize) model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize) @@ -48,15 +48,21 @@ module ActiveRecord::Associations::Builder # :nodoc: foreign_key_was = attribute_before_last_save foreign_key foreign_key = attribute_in_database foreign_key - if foreign_key && model.respond_to?(:increment_counter) - model.increment_counter(cache_column, foreign_key) + if foreign_key && model < ActiveRecord::Base + counter_cache_target(reflection, model, foreign_key).update_counters(cache_column => 1) end - if foreign_key_was && model_was.respond_to?(:decrement_counter) - model_was.decrement_counter(cache_column, foreign_key_was) + if foreign_key_was && model_was < ActiveRecord::Base + counter_cache_target(reflection, model_was, foreign_key_was).update_counters(cache_column => -1) end end end + + private + def counter_cache_target(reflection, model, foreign_key) + primary_key = reflection.association_primary_key(model) + model.unscoped.where!(primary_key => foreign_key) + end end end @@ -84,7 +90,8 @@ module ActiveRecord::Associations::Builder # :nodoc: else klass = association.klass end - old_record = klass.find_by(klass.primary_key => old_foreign_id) + primary_key = reflection.association_primary_key(klass) + old_record = klass.find_by(primary_key => old_foreign_id) if old_record if touch != true @@ -114,9 +121,13 @@ module ActiveRecord::Associations::Builder # :nodoc: BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method) }} - model.after_save callback.(:saved_changes), if: :saved_changes? - model.after_touch callback.(:changes_to_save) - model.after_destroy callback.(:changes_to_save) + unless reflection.counter_cache_column + model.after_create callback.(:saved_changes), if: :saved_changes? + model.after_destroy callback.(:changes_to_save) + end + + model.after_update callback.(:saved_changes), if: :saved_changes? + model.after_touch callback.(:changes_to_save) end def self.add_default_callbacks(model, reflection) 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 1981da11a2..e3070e0472 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 @@ -2,39 +2,6 @@ module ActiveRecord::Associations::Builder # :nodoc: class HasAndBelongsToMany # :nodoc: - class JoinTableResolver # :nodoc: - KnownTable = Struct.new :join_table - - class KnownClass # :nodoc: - def initialize(lhs_class, rhs_class_name) - @lhs_class = lhs_class - @rhs_class_name = rhs_class_name - @join_table = nil - end - - def join_table - @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") - end - - private - - def klass - @lhs_class.send(:compute_type, @rhs_class_name) - end - end - - def self.build(lhs_class, name, options) - if options[:join_table] - KnownTable.new options[:join_table].to_s - else - class_name = options.fetch(:class_name) { - name.to_s.camelize.singularize - } - KnownClass.new lhs_class, class_name.to_s - end - end - end - attr_reader :lhs_model, :association_name, :options def initialize(association_name, lhs_model, options) @@ -44,8 +11,6 @@ module ActiveRecord::Associations::Builder # :nodoc: end def through_model - habtm = JoinTableResolver.build lhs_model, association_name, options - join_model = Class.new(ActiveRecord::Base) { class << self attr_accessor :left_model @@ -56,7 +21,9 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.table_name - table_name_resolver.join_table + # Table name needs to be resolved lazily + # because RHS class might not have been loaded + @table_name ||= table_name_resolver.call end def self.compute_type(class_name) @@ -86,7 +53,7 @@ module ActiveRecord::Associations::Builder # :nodoc: } join_model.name = "HABTM_#{association_name.to_s.camelize}" - join_model.table_name_resolver = habtm + join_model.table_name_resolver = -> { table_name } join_model.left_model = lhs_model join_model.add_left_association :left_side, anonymous_class: lhs_model @@ -117,6 +84,18 @@ module ActiveRecord::Associations::Builder # :nodoc: middle_options end + def table_name + if options[:join_table] + options[:join_table].to_s + else + class_name = options.fetch(:class_name) { + association_name.to_s.camelize.singularize + } + klass = lhs_model.send(:compute_type, class_name.to_s) + [lhs_model.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") + end + end + def belongs_to_options(options) rhs_options = {} diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index ed215fb22c..840d900bbc 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -45,6 +45,8 @@ module ActiveRecord def ids_reader if loaded? target.pluck(reflection.association_primary_key) + elsif !target.empty? + load_target.pluck(reflection.association_primary_key) else @association_ids ||= scope.pluck(reflection.association_primary_key) end @@ -52,11 +54,11 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_type = reflection.association_primary_key_type + primary_key = reflection.association_primary_key + pk_type = klass.type_for_attribute(primary_key) ids = Array(ids).reject(&:blank?) ids.map! { |i| pk_type.cast(i) } - primary_key = reflection.association_primary_key records = klass.where(primary_key => ids).index_by do |r| r.public_send(primary_key) end.values_at(*ids).compact @@ -79,7 +81,13 @@ module ActiveRecord def find(*args) if options[:inverse_of] && loaded? args_flatten = args.flatten - raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank? + model = scope.klass + + if args_flatten.blank? + error_message = "Couldn't find #{model.name} without an ID" + raise RecordNotFound.new(error_message, model.name, model.primary_key, args) + end + result = find_by_scan(*args) result_size = Array(result).size @@ -97,9 +105,7 @@ module ActiveRecord if attributes.is_a?(Array) attributes.collect { |attr| build(attr, &block) } else - add_to_target(build_record(attributes)) do |record| - yield(record) if block_given? - end + add_to_target(build_record(attributes, &block)) end end @@ -206,9 +212,11 @@ module ActiveRecord def size if !find_target? || loaded? target.size + elsif @association_ids + @association_ids.size elsif !association_scope.group_values.empty? load_target.size - elsif !association_scope.distinct_value && target.is_a?(Array) + elsif !association_scope.distinct_value && !target.empty? unsaved_records = target.select(&:new_record?) unsaved_records.size + count_records else @@ -225,10 +233,10 @@ module ActiveRecord # loaded and you are going to fetch the records anyway it is better to # check <tt>collection.length.zero?</tt>. def empty? - if loaded? + if loaded? || @association_ids size.zero? else - @target.blank? && !scope.exists? + target.empty? && !scope.exists? end end @@ -350,15 +358,18 @@ module ActiveRecord if attributes.is_a?(Array) attributes.collect { |attr| _create_record(attr, raise, &block) } else + record = build_record(attributes, &block) transaction do - add_to_target(build_record(attributes)) do |record| - yield(record) if block_given? - insert_record(record, true, raise) { + result = nil + add_to_target(record) do + result = insert_record(record, true, raise) { @_was_loaded = loaded? @association_ids = nil } end + raise ActiveRecord::Rollback unless result end + record end end @@ -389,7 +400,7 @@ module ActiveRecord records.each { |record| callback(:before_remove, record) } delete_records(existing_records, method) if existing_records.any? - records.each { |record| target.delete(record) } + @target -= records records.each { |record| callback(:after_remove, record) } end @@ -436,7 +447,9 @@ module ActiveRecord end end - result && records + raise ActiveRecord::Rollback unless result + + records end def replace_on_target(record, index, skip_callbacks) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 8b4a48a38c..9a30198b95 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -32,7 +32,7 @@ module ActiveRecord class CollectionProxy < Relation def initialize(klass, association) #:nodoc: @association = association - super klass, klass.arel_table, klass.predicate_builder + super klass extensions = association.extensions extend(*extensions) if extensions.any? diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 07c7f28d2d..cf85a87fa7 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -63,7 +63,7 @@ module ActiveRecord count = if reflection.has_cached_counter? owner._read_attribute(reflection.counter_cache_column).to_i else - scope.count + scope.count(:all) end # If there's nothing in the database and @target has no new records 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 adbf52b87c..617956c768 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -8,9 +8,7 @@ module ActiveRecord def initialize(owner, reflection) super - - @through_records = {} - @through_association = nil + @through_records = {} end def concat(*records) @@ -50,11 +48,6 @@ module ActiveRecord end private - - def through_association - @through_association ||= owner.association(through_reflection.name) - end - # The through record (built with build_record) is temporarily cached # so that it may be reused if insert_record is subsequently called. # @@ -97,7 +90,7 @@ module ActiveRecord def build_record(attributes) ensure_not_nested - record = super(attributes) + record = super inverse = source_reflection.inverse_of if inverse @@ -140,21 +133,15 @@ module ActiveRecord scope = through_association.scope scope.where! construct_join_attributes(*records) + scope = scope.where(through_scope_attributes) case method when :destroy if scope.klass.primary_key - count = scope.destroy_all.length + count = scope.destroy_all.count(&:destroyed?) else scope.each(&:_run_destroy_callbacks) - - arel = scope.arel - - stmt = Arel::DeleteManager.new - stmt.from scope.klass.arel_table - stmt.wheres = arel.constraints - - count = scope.klass.connection.delete(stmt, "SQL") + count = scope.delete_all end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 7953b89f61..390bfd8b08 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -23,35 +23,6 @@ module ActiveRecord end end - def replace(record, save = true) - raise_on_type_mismatch!(record) if record - load_target - - return target unless target || record - - assigning_another_record = target != record - if assigning_another_record || record.has_changes_to_save? - save &&= owner.persisted? - - transaction_if(save) do - remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record - - if record - set_owner_attributes(record) - set_inverse_instance(record) - - if save && !record.save - nullify_owner_attributes(record) - set_owner_attributes(target) if target - raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." - end - end - end - end - - self.target = record - end - def delete(method = options[:dependent]) if load_target case method @@ -60,6 +31,7 @@ module ActiveRecord when :destroy target.destroyed_by_association = reflection target.destroy + throw(:abort) unless target.destroyed? when :nullify target.update_columns(reflection.foreign_key => nil) if target.persisted? end @@ -67,6 +39,33 @@ module ActiveRecord end private + def replace(record, save = true) + raise_on_type_mismatch!(record) if record + + return target unless load_target || record + + assigning_another_record = target != record + if assigning_another_record || record.has_changes_to_save? + save &&= owner.persisted? + + transaction_if(save) do + remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record + + if record + set_owner_attributes(record) + set_inverse_instance(record) + + if save && !record.save + nullify_owner_attributes(record) + set_owner_attributes(target) if target + raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." + end + end + end + end + + self.target = record + end # The reason that the save param for replace is false, if for create (not just build), # is because the setting of the foreign keys is actually handled by the scoping when @@ -106,6 +105,14 @@ module ActiveRecord yield end end + + def _create_record(attributes, raise_error = false, &block) + unless owner.persisted? + raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" + end + + super + end end end end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 36746f9115..10978b2d93 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -6,17 +6,16 @@ module ActiveRecord class HasOneThroughAssociation < HasOneAssociation #:nodoc: include ThroughAssociation - def replace(record) - create_through_record(record) - self.target = record - end - private + def replace(record, save = true) + create_through_record(record, save) + self.target = record + end - def create_through_record(record) + def create_through_record(record, save) ensure_not_nested - through_proxy = owner.association(through_reflection.name) + through_proxy = through_association through_record = through_proxy.load_target if through_record && !record @@ -29,8 +28,12 @@ module ActiveRecord end if through_record - through_record.update(attributes) - elsif owner.new_record? + if through_record.new_record? + through_record.assign_attributes(attributes) + else + through_record.update(attributes) + end + elsif owner.new_record? || !save through_proxy.build(attributes) else through_proxy.create(attributes) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index df4bf07999..b76005b587 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -14,10 +14,8 @@ module ActiveRecord i[column.name] = column.alias } } - @name_and_alias_cache = tables.each_with_object({}) { |table, h| - h[table.node] = table.columns.map { |column| - [column.name, column.alias] - } + @columns_cache = tables.each_with_object({}) { |table, h| + h[table.node] = table.columns } end @@ -25,9 +23,8 @@ module ActiveRecord @tables.flat_map(&:column_aliases) end - # An array of [column_name, alias] pairs for the table def column_aliases(node) - @name_and_alias_cache[node] + @columns_cache[node] end def column_alias(node, column) @@ -67,64 +64,31 @@ module ActiveRecord end end - # base is the base class on which operation is taking place. - # associations is the list of associations which are joined using hash, symbol or array. - # joins is the list of all string join commands and arel nodes. - # - # Example : - # - # class Physician < ActiveRecord::Base - # has_many :appointments - # has_many :patients, through: :appointments - # end - # - # If I execute `@physician.patients.to_a` then - # base # => Physician - # associations # => [] - # joins # => [#<Arel::Nodes::InnerJoin: ...] - # - # However if I execute `Physician.joins(:appointments).to_a` then - # base # => Physician - # associations # => [:appointments] - # joins # => [] - # - def initialize(base, table, associations, alias_tracker, eager_loading: true) - @alias_tracker = alias_tracker - @eager_loading = eager_loading + def initialize(base, table, associations) tree = self.class.make_tree associations @join_root = JoinBase.new(base, table, build(tree, base)) - @join_root.children.each { |child| construct_tables! @join_root, child } end def reflections join_root.drop(1).map!(&:reflection) end - def join_constraints(joins_to_add, join_type) - joins = join_root.children.flat_map { |child| - make_join_constraints(join_root, child, join_type) - } + def join_constraints(joins_to_add, join_type, alias_tracker) + @alias_tracker = alias_tracker + + construct_tables!(join_root) + joins = make_join_constraints(join_root, join_type) joins.concat joins_to_add.flat_map { |oj| + construct_tables!(oj.join_root) if join_root.match? oj.join_root walk join_root, oj.join_root else - oj.join_root.children.flat_map { |child| - make_join_constraints(oj.join_root, child, join_type) - } + make_join_constraints(oj.join_root, join_type) end } end - def aliases - @aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i| - columns = join_part.column_names.each_with_index.map { |column_name, j| - Aliases::Column.new column_name, "t#{i}_r#{j}" - } - Aliases::Table.new(join_part, columns) - } - end - def instantiate(result_set, &block) primary_key = aliases.column_alias(join_root, join_root.primary_key) @@ -149,35 +113,49 @@ module ActiveRecord result_set.each { |row_hash| parent_key = primary_key ? row_hash[primary_key] : row_hash parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block) - construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + construct(parent, join_root, row_hash, seen, model_cache) } end parents.values end + def apply_column_aliases(relation) + relation._select!(-> { aliases.columns }) + end + protected - attr_reader :alias_tracker, :base_klass, :join_root + attr_reader :join_root private + attr_reader :alias_tracker - def make_constraints(parent, child, tables, join_type) - chain = child.reflection.chain - foreign_table = parent.table - foreign_klass = parent.base_klass - child.join_constraints(foreign_table, foreign_klass, join_type, tables, chain) + def aliases + @aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i| + columns = join_part.column_names.each_with_index.map { |column_name, j| + Aliases::Column.new column_name, "t#{i}_r#{j}" + } + Aliases::Table.new(join_part, columns) + } end - def make_outer_joins(parent, child) - join_type = Arel::Nodes::OuterJoin - make_join_constraints(parent, child, join_type, true) + def construct_tables!(join_root) + join_root.each_children do |parent, child| + child.tables = table_aliases_for(parent, child) + end end - def make_join_constraints(parent, child, join_type, aliasing = false) - tables = aliasing ? table_aliases_for(parent, child) : child.tables - joins = make_constraints(parent, child, tables, join_type) + def make_join_constraints(join_root, join_type) + join_root.children.flat_map do |child| + make_constraints(join_root, child, join_type) + end + end - joins.concat child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) } + def make_constraints(parent, child, join_type = Arel::Nodes::OuterJoin) + foreign_table = parent.table + foreign_klass = parent.base_klass + joins = child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) + joins.concat child.children.flat_map { |c| make_constraints(child, c, join_type) } end def table_aliases_for(parent, node) @@ -190,13 +168,8 @@ module ActiveRecord } end - def construct_tables!(parent, node) - node.tables = table_aliases_for(parent, node) - node.children.each { |child| construct_tables! node, child } - end - def table_alias_for(reflection, parent, join) - name = "#{reflection.plural_name}_#{parent.table_name}" + name = reflection.alias_candidate(parent.table_name) join ? "#{name}_join" : name end @@ -205,8 +178,8 @@ module ActiveRecord [left.children.find { |node2| node1.match? node2 }, node1] }.partition(&:first) - ojs = missing.flat_map { |_, n| make_outer_joins left, n } - intersection.flat_map { |l, r| walk l, r }.concat ojs + joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r) } + joins.concat missing.flat_map { |_, n| make_constraints(left, n) } end def find_reflection(klass, name) @@ -221,15 +194,14 @@ module ActiveRecord reflection.check_eager_loadable! if reflection.polymorphic? - next unless @eager_loading raise EagerLoadPolymorphicError.new(reflection) end - JoinAssociation.new(reflection, build(right, reflection.klass), alias_tracker) - end.compact + JoinAssociation.new(reflection, build(right, reflection.klass)) + end end - def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + def construct(ar_parent, parent, row, seen, model_cache) return if ar_parent.nil? parent.children.each do |node| @@ -238,7 +210,7 @@ module ActiveRecord other.loaded! elsif ar_parent.association_cached?(node.reflection.name) model = ar_parent.association(node.reflection.name).target - construct(model, node, row, rs, seen, model_cache, aliases) + construct(model, node, row, seen, model_cache) next end @@ -250,25 +222,20 @@ module ActiveRecord next end - model = seen[ar_parent.object_id][node.base_klass][id] + model = seen[ar_parent.object_id][node][id] if model - construct(model, node, row, rs, seen, model_cache, aliases) + construct(model, node, row, seen, model_cache) else - model = construct_model(ar_parent, node, row, model_cache, id, aliases) - - if node.reflection.scope && - node.reflection.scope_for(node.base_klass.unscoped).readonly_value - model.readonly! - end + model = construct_model(ar_parent, node, row, model_cache, id) - seen[ar_parent.object_id][node.base_klass][id] = model - construct(model, node, row, rs, seen, model_cache, aliases) + seen[ar_parent.object_id][node][id] = model + construct(model, node, row, seen, model_cache) end end end - def construct_model(record, node, row, model_cache, id, aliases) + def construct_model(record, node, row, model_cache, id) other = record.association(node.reflection.name) model = model_cache[node][id] ||= @@ -282,6 +249,7 @@ module ActiveRecord other.target = model end + model.readonly! if node.readonly? model end 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 221c791bf8..4583d89cba 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -6,17 +6,14 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinAssociation < JoinPart # :nodoc: - # The reflection of the association represented - attr_reader :reflection + attr_reader :reflection, :tables + attr_accessor :table - attr_accessor :tables - - def initialize(reflection, children, alias_tracker) + def initialize(reflection, children) super(reflection.klass, children) - @alias_tracker = alias_tracker - @reflection = reflection - @tables = nil + @reflection = reflection + @tables = nil end def match?(other) @@ -24,14 +21,13 @@ module ActiveRecord super && reflection == other.reflection end - def join_constraints(foreign_table, foreign_klass, join_type, tables, chain) - joins = [] - tables = tables.reverse + def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) + joins = [] # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse - chain.reverse_each do |reflection| - table = tables.shift + reflection.chain.reverse_each.with_index(1) do |reflection, i| + table = tables[-i] klass = reflection.klass constraint = reflection.build_join_constraint(table, foreign_table) @@ -54,12 +50,16 @@ module ActiveRecord joins end - def table - tables.first + def tables=(tables) + @tables = tables + @table = tables.first end - protected - attr_reader :alias_tracker + def readonly? + return @readonly if defined?(@readonly) + + @readonly = reflection.scope && reflection.scope_for(base_klass.unscoped).readonly_value + end end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 2181f308bf..3ad72a3646 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -33,6 +33,13 @@ module ActiveRecord children.each { |child| child.each(&block) } end + def each_children(&block) + children.each do |child| + yield self, child + child.each_children(&block) + end + end + # An Arel::Table for the active_record def table raise NotImplementedError @@ -47,8 +54,8 @@ module ActiveRecord length = column_names_with_alias.length while index < length - column_name, alias_name = column_names_with_alias[index] - hash[column_name] = row[alias_name] + column = column_names_with_alias[index] + hash[column.name] = row[column.alias] index += 1 end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index e1087be9b3..5c2ac5b374 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -83,7 +83,7 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] def preload(records, associations, preload_scope = nil) - records = records.compact + records = Array.wrap(records).compact if records.empty? [] @@ -98,26 +98,30 @@ module ActiveRecord private # Loads all the given data into +records+ for the +association+. - def preloaders_on(association, records, scope) + def preloaders_on(association, records, scope, polymorphic_parent = false) case association when Hash - preloaders_for_hash(association, records, scope) + preloaders_for_hash(association, records, scope, polymorphic_parent) when Symbol - preloaders_for_one(association, records, scope) + preloaders_for_one(association, records, scope, polymorphic_parent) when String - preloaders_for_one(association.to_sym, records, scope) + preloaders_for_one(association.to_sym, records, scope, polymorphic_parent) else raise ArgumentError, "#{association.inspect} was not recognized for preload" end end - def preloaders_for_hash(association, records, scope) + def preloaders_for_hash(association, records, scope, polymorphic_parent) association.flat_map { |parent, child| - loaders = preloaders_for_one parent, records, scope + loaders = preloaders_for_one parent, records, scope, polymorphic_parent recs = loaders.flat_map(&:preloaded_records).uniq + + reflection = records.first.class._reflect_on_association(parent) + polymorphic_parent = reflection && reflection.options[:polymorphic] + loaders.concat Array.wrap(child).flat_map { |assoc| - preloaders_on assoc, recs, scope + preloaders_on assoc, recs, scope, polymorphic_parent } loaders } @@ -135,8 +139,8 @@ module ActiveRecord # Additionally, polymorphic belongs_to associations can have multiple associated # classes, depending on the polymorphic_type field. So we group by the classes as # well. - def preloaders_for_one(association, records, scope) - grouped_records(association, records).flat_map do |reflection, klasses| + def preloaders_for_one(association, records, scope, polymorphic_parent) + grouped_records(association, records, polymorphic_parent).flat_map do |reflection, klasses| klasses.map do |rhs_klass, rs| loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope) loader.run self @@ -145,10 +149,11 @@ module ActiveRecord end end - def grouped_records(association, records) + def grouped_records(association, records, polymorphic_parent) h = {} records.each do |record| next unless record + next if polymorphic_parent && !record.class._reflect_on_association(association) assoc = record.association(association) next unless assoc.klass klasses = h[assoc.reflection] ||= {} @@ -169,7 +174,7 @@ module ActiveRecord owners.flat_map { |owner| owner.association(reflection.name).target } end - protected + private attr_reader :owners, :reflection end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 735da152b7..d6f7359055 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -27,10 +27,9 @@ module ActiveRecord end end - protected + private attr_reader :owners, :reflection, :preload_scope, :model, :klass - private # The name of the key on the associated records def association_key_name reflection.join_primary_key(klass) @@ -43,11 +42,11 @@ module ActiveRecord def associate_records_to_owner(owner, records) association = owner.association(reflection.name) + association.loaded! if reflection.collection? - association.loaded! association.target.concat(records) else - association.target = records.first + association.target = records.first unless records.empty? end end @@ -82,11 +81,11 @@ module ActiveRecord end def association_key_type - @klass.type_for_attribute(association_key_name.to_s).type + @klass.type_for_attribute(association_key_name).type end def owner_key_type - @model.type_for_attribute(owner_key_name.to_s).type + @model.type_for_attribute(owner_key_name).type end def load_records(&block) @@ -118,7 +117,7 @@ module ActiveRecord scope = klass.scope_for_association if reflection.type - scope.where!(reflection.type => model.base_class.sti_name) + scope.where!(reflection.type => model.polymorphic_name) end scope.merge!(reflection_scope) if reflection.scope diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 441bd715e4..cfab16a745 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -17,9 +17,8 @@ module ActiveRecord replace(record) end - def build(attributes = {}) - record = build_record(attributes) - yield(record) if block_given? + def build(attributes = {}, &block) + record = build_record(attributes, &block) set_new_record(record) record end @@ -62,13 +61,8 @@ module ActiveRecord replace(record) end - def _create_record(attributes, raise_error = false) - unless owner.persisted? - raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" - end - - record = build_record(attributes) - yield(record) if block_given? + def _create_record(attributes, raise_error = false, &block) + record = build_record(attributes, &block) saved = record.save set_new_record(record) raise RecordInvalid.new(record) if !saved && raise_error diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index bce2a95ce1..15e6565e69 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -4,9 +4,24 @@ module ActiveRecord module Associations # = Active Record Through Association module ThroughAssociation #:nodoc: - delegate :source_reflection, :through_reflection, to: :reflection + delegate :source_reflection, to: :reflection private + def through_reflection + @through_reflection ||= begin + refl = reflection.through_reflection + + while refl.through_reflection? + refl = refl.through_reflection + end + + refl + end + end + + def through_association + @through_association ||= owner.association(through_reflection.name) + end # We merge in these scopes for two reasons: # @@ -38,24 +53,22 @@ module ActiveRecord def construct_join_attributes(*records) ensure_mutable - if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key + association_primary_key = source_reflection.association_primary_key(reflection.klass) + + if association_primary_key == reflection.klass.primary_key && !options[:source_type] join_attributes = { source_reflection.name => records } else join_attributes = { - source_reflection.foreign_key => - records.map { |record| - record.send(source_reflection.association_primary_key(reflection.klass)) - } + source_reflection.foreign_key => records.map(&association_primary_key.to_sym) } end if options[:source_type] - join_attributes[source_reflection.foreign_type] = - records.map { |record| record.class.base_class.name } + join_attributes[source_reflection.foreign_type] = [ options[:source_type] ] end if records.count == 1 - Hash[join_attributes.map { |k, v| [k, v.first] }] + join_attributes.transform_values!(&:first) else join_attributes end @@ -101,7 +114,7 @@ module ActiveRecord attributes[inverse.foreign_key] = target.id end - super(attributes) + super end end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 8b0d9aab01..b6f0e18764 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -4,14 +4,8 @@ require "active_model/forbidden_attributes_protection" module ActiveRecord module AttributeAssignment - extend ActiveSupport::Concern include ActiveModel::AttributeAssignment - # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment. - def attributes=(attributes) - assign_attributes(attributes) - end - private def _assign_attributes(attributes) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 23d2aef214..e4b8b1a330 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -26,7 +26,7 @@ module ActiveRecord def self.set_name_cache(name, value) const_name = "ATTR_#{name}" unless const_defined? const_name - const_set const_name, value.dup.freeze + const_set const_name, -value end end } @@ -59,7 +59,7 @@ module ActiveRecord # attribute methods. generated_attribute_methods.synchronize do return false if @attribute_methods_generated - superclass.define_attribute_methods unless self == base_class + superclass.define_attribute_methods unless base_class? super(attribute_names) @attribute_methods_generated = true end @@ -167,6 +167,57 @@ module ActiveRecord end end + # Regexp whitelist. Matches the following: + # "#{table_name}.#{column_name}" + # "#{column_name}" + COLUMN_NAME_WHITELIST = /\A(?:\w+\.)?\w+\z/i + + # Regexp whitelist. Matches the following: + # "#{table_name}.#{column_name}" + # "#{table_name}.#{column_name} #{direction}" + # "#{table_name}.#{column_name} #{direction} NULLS FIRST" + # "#{table_name}.#{column_name} NULLS LAST" + # "#{column_name}" + # "#{column_name} #{direction}" + # "#{column_name} #{direction} NULLS FIRST" + # "#{column_name} NULLS LAST" + COLUMN_NAME_ORDER_WHITELIST = / + \A + (?:\w+\.)? + \w+ + (?:\s+asc|\s+desc)? + (?:\s+nulls\s+(?:first|last))? + \z + /ix + + def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc: + unexpected = args.reject do |arg| + arg.kind_of?(Arel::Node) || + arg.is_a?(Arel::Nodes::SqlLiteral) || + arg.is_a?(Arel::Attributes::Attribute) || + arg.to_s.split(/\s*,\s*/).all? { |part| whitelist.match?(part) } + end + + return if unexpected.none? + + if allow_unsafe_raw_sql == :deprecated + ActiveSupport::Deprecation.warn( + "Dangerous query method (method whose arguments are used as raw " \ + "SQL) called with non-attribute argument(s): " \ + "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \ + "arguments will be disallowed in Rails 6.0. This method should " \ + "not be called with user-provided values, such as request " \ + "parameters or model attributes. Known-safe values can be passed " \ + "by wrapping them in Arel.sql()." + ) + else + raise(ActiveRecord::UnknownAttributeReference, + "Query method called with non-attribute argument(s): " + + unexpected.map(&:inspect).join(", ") + ) + end + end + # Returns true if the given attribute exists, otherwise false. # # class Person < ActiveRecord::Base @@ -392,33 +443,24 @@ module ActiveRecord @attributes.accessed end - protected - - def attribute_method?(attr_name) # :nodoc: + private + def attribute_method?(attr_name) # We check defined? because Syck calls respond_to? before actually calling initialize. defined?(@attributes) && @attributes.key?(attr_name) end - private - - def arel_attributes_with_values_for_create(attribute_names) - arel_attributes_with_values(attributes_for_create(attribute_names)) + def attributes_with_values_for_create(attribute_names) + attributes_with_values(attributes_for_create(attribute_names)) end - def arel_attributes_with_values_for_update(attribute_names) - arel_attributes_with_values(attributes_for_update(attribute_names)) + def attributes_with_values_for_update(attribute_names) + attributes_with_values(attributes_for_update(attribute_names)) end - # Returns a Hash of the Arel::Attributes and attribute values that have been - # typecasted for use in an Arel insert/update method. - def arel_attributes_with_values(attribute_names) - attrs = {} - arel_table = self.class.arel_table - - attribute_names.each do |name| - attrs[arel_table[name]] = typecasted_attribute_value(name) + def attributes_with_values(attribute_names) + attribute_names.each_with_object({}) do |name, attrs| + attrs[name] = _read_attribute(name) end - attrs end # Filters the primary keys and readonly attributes from the attribute names. @@ -443,9 +485,5 @@ module ActiveRecord def pk_attribute?(name) name == self.class.primary_key end - - def typecasted_attribute_value(name) - _read_attribute(name) - end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 3de6fe566d..233ee29fac 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -39,11 +39,12 @@ module ActiveRecord end end - # Did this attribute change when we last saved? This method can be invoked - # as +saved_change_to_name?+ instead of <tt>saved_change_to_attribute?("name")</tt>. - # Behaves similarly to +attribute_changed?+. This method is useful in - # after callbacks to determine if the call to save changed a certain - # attribute. + # Did this attribute change when we last saved? + # + # This method is useful in after callbacks to determine if an attribute + # was changed during the save that triggered the callbacks to run. It can + # be invoked as +saved_change_to_name?+ instead of + # <tt>saved_change_to_attribute?("name")</tt>. # # ==== Options # @@ -60,19 +61,20 @@ module ActiveRecord # attribute was changed, the result will be an array containing the # original value and the saved value. # - # Behaves similarly to +attribute_change+. This method is useful in after - # callbacks, to see the change in an attribute that just occurred - # - # This method can be invoked as +saved_change_to_name+ in instead of - # <tt>saved_change_to_attribute("name")</tt> + # This method is useful in after callbacks, to see the change in an + # attribute during the save that triggered the callbacks to run. It can be + # invoked as +saved_change_to_name+ instead of + # <tt>saved_change_to_attribute("name")</tt>. def saved_change_to_attribute(attr_name) mutations_before_last_save.change_to_attribute(attr_name) end # Returns the original value of an attribute before the last save. - # Behaves similarly to +attribute_was+. This method is useful in after - # callbacks to get the original value of an attribute before the save that - # just occurred + # + # This method is useful in after callbacks to get the original value of an + # attribute before the save that triggered the callbacks to run. It can be + # invoked as +name_before_last_save+ instead of + # <tt>attribute_before_last_save("name")</tt>. def attribute_before_last_save(attr_name) mutations_before_last_save.original_value(attr_name) end @@ -87,39 +89,75 @@ module ActiveRecord mutations_before_last_save.changes end - # Alias for +attribute_changed?+ + # Will this attribute change the next time we save? + # + # This method is useful in validations and before callbacks to determine + # if the next call to +save+ will change a particular attribute. It can be + # invoked as +will_save_change_to_name?+ instead of + # <tt>will_save_change_to_attribute("name")</tt>. + # + # ==== Options + # + # +from+ When passed, this method will return false unless the original + # value is equal to the given option + # + # +to+ When passed, this method will return false unless the value will be + # changed to the given value def will_save_change_to_attribute?(attr_name, **options) mutations_from_database.changed?(attr_name, **options) end - # Alias for +attribute_change+ + # Returns the change to an attribute that will be persisted during the + # next save. + # + # This method is useful in validations and before callbacks, to see the + # change to an attribute that will occur when the record is saved. It can + # be invoked as +name_change_to_be_saved+ instead of + # <tt>attribute_change_to_be_saved("name")</tt>. + # + # If the attribute will change, the result will be an array containing the + # original value and the new value about to be saved. def attribute_change_to_be_saved(attr_name) mutations_from_database.change_to_attribute(attr_name) end - # Alias for +attribute_was+ + # Returns the value of an attribute in the database, as opposed to the + # in-memory value that will be persisted the next time the record is + # saved. + # + # This method is useful in validations and before callbacks, to see the + # original value of an attribute prior to any changes about to be + # saved. It can be invoked as +name_in_database+ instead of + # <tt>attribute_in_database("name")</tt>. def attribute_in_database(attr_name) mutations_from_database.original_value(attr_name) end - # Alias for +changed?+ + # Will the next call to +save+ have any changes to persist? def has_changes_to_save? mutations_from_database.any_changes? end - # Alias for +changes+ + # Returns a hash containing all the changes that will be persisted during + # the next save. def changes_to_save mutations_from_database.changes end - # Alias for +changed+ + # Returns an array of the names of any attributes that will change when + # the record is next saved. def changed_attribute_names_to_save - changes_to_save.keys + mutations_from_database.changed_attribute_names end - # Alias for +changed_attributes+ + # Returns a hash of the attributes that will change when the record is + # next saved. + # + # The hash keys are the attribute names, and the hash values are the + # original attribute values in the database (as opposed to the in-memory + # values about to be saved). def attributes_in_database - changes_to_save.transform_values(&:first) + mutations_from_database.changed_values end private diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index d8fc046e10..9b267bb7c0 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -83,7 +83,7 @@ module ActiveRecord end def reset_primary_key #:nodoc: - if self == base_class + if base_class? self.primary_key = get_primary_key(base_class.name) else self.primary_key = base_class.primary_key @@ -131,7 +131,7 @@ module ActiveRecord def suppress_composite_primary_key(pk) return pk unless pk.is_a?(Array) - warn <<-WARNING.strip_heredoc + warn <<~WARNING WARNING: Active Record does not support composite primary key. #{table_name} has composite primary key. Composite primary key is ignored. diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 4077250583..0f7bcba564 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -27,7 +27,7 @@ module ActiveRecord # Making it frozen means that it doesn't get duped when used to # key the @attributes in read_attribute. def define_method_attribute(name) - safe_name = name.unpack("h*".freeze).first + safe_name = name.unpack1("h*".freeze) temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name @@ -69,7 +69,7 @@ module ActiveRecord 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 + def _read_attribute(attr_name, &block) # :nodoc: @attributes.fetch_value(attr_name.to_s, &block) end else diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index ebc2baed34..6e0e90f39c 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -7,7 +7,7 @@ module ActiveRecord class ColumnNotSerializableError < StandardError def initialize(name, type) - super <<-EOS.strip_heredoc + super <<~EOS Column `#{name}` of type #{type.class} does not support `serialize` feature. Usually it means that you are trying to use `serialize` on a column that already implements serialization natively. 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 d2b7817b45..294a3dc32c 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -73,7 +73,7 @@ module ActiveRecord # `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| + decorate_matching_attribute_types(matcher, "_time_zone_conversion") do |type| TimeZoneConverter.new(type) end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index bb0ec6a8c3..c7521422bb 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -13,7 +13,7 @@ module ActiveRecord private def define_method_attribute=(name) - safe_name = name.unpack("h*".freeze).first + safe_name = name.unpack1("h*".freeze) ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 0b7c9398a8..35150889d9 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -57,7 +57,7 @@ module ActiveRecord # store_listing = StoreListing.new(price_in_cents: '10.1') # # # before - # store_listing.price_in_cents # => BigDecimal.new(10.1) + # store_listing.price_in_cents # => BigDecimal(10.1) # # class StoreListing < ActiveRecord::Base # attribute :price_in_cents, :integer diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 6974cf74f6..a405f05e0b 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -392,7 +392,7 @@ module ActiveRecord records -= records_to_destroy end - records.each do |record| + records.each_with_index do |record, index| next if record.destroyed? saved = true @@ -400,8 +400,13 @@ module ActiveRecord if autosave != false && (@new_record_before_save || record.new_record?) if autosave saved = association.insert_record(record, false) - else - association.insert_record(record) unless reflection.nested? + elsif !reflection.nested? + if reflection.validate? + valid = association_valid?(reflection, record, index) + saved = valid ? association.insert_record(record, false) : false + else + association.insert_record(record) + end end elsif autosave saved = record.save(validate: false) @@ -436,6 +441,9 @@ module ActiveRecord if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key) unless reflection.through_reflection record[reflection.foreign_key] = key + if inverse_reflection = reflection.inverse_of + record.association(inverse_reflection.name).loaded! + end end saved = record.save(validate: !autosave) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index b7ad944cec..5169f312f5 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -9,7 +9,6 @@ require "active_support/core_ext/module/attribute_accessors" 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" @@ -289,8 +288,10 @@ module ActiveRecord #:nodoc: extend Enum extend Delegation::DelegateCache extend CollectionCacheKey + extend Aggregations::ClassMethods include Core + include DatabaseConfigurations include Persistence include ReadonlyAttributes include ModelSchema @@ -314,7 +315,6 @@ module ActiveRecord #:nodoc: include ActiveModel::SecurePassword include AutosaveAssociation include NestedAttributes - include Aggregations include Transactions include TouchLater include NoTouching diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 9ab2780760..b6852bfc71 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -75,21 +75,7 @@ module ActiveRecord # end # # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is - # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation - # where the +before_destroy+ method is overridden: - # - # class Topic < ActiveRecord::Base - # def before_destroy() destroy_author end - # end - # - # class Reply < Topic - # def before_destroy() destroy_readers end - # end - # - # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. - # So, use the callback macros when you want to ensure that a certain callback is called for the entire - # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant - # to decide whether they want to call +super+ and trigger the inherited callbacks. + # run, both +destroy_author+ and +destroy_readers+ are called. # # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the # callbacks before specifying the associations. Otherwise, you might trigger the loading of a @@ -142,7 +128,7 @@ module ActiveRecord # end # end # - # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has + # So you specify the object you want to be messaged on a given callback. When that callback is triggered, the object has # a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other # initialization data such as the name of the attribute to work with: # @@ -232,7 +218,7 @@ module ActiveRecord # # For example: # - # class Topic + # class Topic < ActiveRecord::Base # has_many :children # # after_save :log_children @@ -257,7 +243,7 @@ module ActiveRecord # # For example: # - # class Topic + # class Topic < ActiveRecord::Base # has_many :children # # after_commit :log_children @@ -332,6 +318,10 @@ module ActiveRecord _run_touch_callbacks { super } end + def increment!(*, touch: nil) # :nodoc: + touch ? _run_touch_callbacks { super } : super + end + private def create_or_update(*) diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 88b398ad45..dfba78614e 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -3,11 +3,11 @@ module ActiveRecord module CollectionCacheKey def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc: - query_signature = Digest::MD5.hexdigest(collection.to_sql) + query_signature = ActiveSupport::Digest.hexdigest(collection.to_sql) key = "#{collection.model_name.cache_key}/query-#{query_signature}" - if collection.loaded? - size = collection.size + if collection.loaded? || collection.distinct_value + size = collection.records.size if size > 0 timestamp = collection.max_by(×tamp_column)._read_attribute(timestamp_column) end @@ -15,13 +15,12 @@ module ActiveRecord if collection.eager_loading? collection = collection.send(:apply_join_dependency) end - column_type = type_for_attribute(timestamp_column.to_s) + column_type = type_for_attribute(timestamp_column) column = connection.column_name_from_arel_node(collection.arel_attribute(timestamp_column)) select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp" if collection.has_limit_or_offset? - query = collection.spawn - query.select_values = [column] + query = collection.select(column) subquery_alias = "subquery_for_cache_key" subquery_column = "#{subquery_alias}.#{timestamp_column}" subquery = query.arel.as(subquery_alias) 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 0759f4d2b3..f721e91203 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -63,15 +63,13 @@ module ActiveRecord # There are several connection-pooling-related options that you can add to # your database connection configuration: # - # * +pool+: number indicating size of connection pool (default 5) - # * +checkout_timeout+: number of seconds to block and wait for a connection - # before giving up and raising a timeout error (default 5 seconds). - # * +reaping_frequency+: frequency in seconds to periodically run the - # Reaper, which attempts to find and recover connections from dead - # threads, which can occur if a programmer forgets to close a - # connection at the end of a thread or a thread dies unexpectedly. - # Regardless of this setting, the Reaper will be invoked before every - # blocking wait. (Default +nil+, which means don't schedule the Reaper). + # * +pool+: maximum number of connections the pool may manage (default 5). + # * +idle_timeout+: number of seconds that a connection will be kept + # unused in the pool before it is automatically disconnected (default + # 300 seconds). Set this to zero to keep connections forever. + # * +checkout_timeout+: number of seconds to wait for a connection to + # become available before giving up and raising a timeout error (default + # 5 seconds). # #-- # Synchronization policy: @@ -82,11 +80,8 @@ module ActiveRecord # * private methods that require being called in a +synchronize+ blocks # are now explicitly documented class ConnectionPool - # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool - # with which it shares a Monitor. But could be a generic Queue. - # - # The Queue in stdlib's 'thread' could replace this class except - # stdlib's doesn't support waiting with a timeout. + # Threadsafe, fair, LIFO queue. Meant to be used by ConnectionPool + # with which it shares a Monitor. class Queue def initialize(lock = Monitor.new) @lock = lock @@ -175,7 +170,7 @@ module ActiveRecord # Removes and returns the head of the queue if possible, or +nil+. def remove - @queue.shift + @queue.pop end # Remove and return the head the queue if the number of @@ -283,12 +278,12 @@ module ActiveRecord end end - # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. - # A reaper instantiated with a +nil+ frequency will never reap the - # connection pool. + # Every +frequency+ seconds, the reaper will call +reap+ and +flush+ on + # +pool+. A reaper instantiated with a zero frequency will never reap + # the connection pool. # - # Configure the frequency by setting "reaping_frequency" in your - # database yaml file. + # Configure the frequency by setting +reaping_frequency+ in your database + # yaml file (default 60 seconds). class Reaper attr_reader :pool, :frequency @@ -298,11 +293,12 @@ module ActiveRecord end def run - return unless frequency + return unless frequency && frequency > 0 Thread.new(frequency, pool) { |t, p| loop do sleep t p.reap + p.flush end } end @@ -326,6 +322,10 @@ module ActiveRecord @spec = spec @checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5 + if @idle_timeout = spec.config.fetch(:idle_timeout, 300) + @idle_timeout = @idle_timeout.to_f + @idle_timeout = nil if @idle_timeout <= 0 + end # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 @@ -356,7 +356,10 @@ module ActiveRecord @lock_thread = false - @reaper = Reaper.new(self, spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f) + # +reaping_frequency+ is configurable mostly for historical reasons, but it could + # also be useful if someone wants a very low +idle_timeout+. + reaping_frequency = spec.config.fetch(:reaping_frequency, 60) + @reaper = Reaper.new(self, reaping_frequency && reaping_frequency.to_f) @reaper.run end @@ -450,6 +453,21 @@ module ActiveRecord disconnect(false) end + # Discards all connections in the pool (even if they're currently + # leased!), along with the pool itself. Any further interaction with the + # pool (except #spec and #schema_cache) is undefined. + # + # See AbstractAdapter#discard! + def discard! # :nodoc: + synchronize do + return if @connections.nil? # already discarded + @connections.each do |conn| + conn.discard! + end + @connections = @available = @thread_cached_conns = nil + end + end + # Clears the cache which maps classes and re-connects connections that # require reloading. # @@ -575,6 +593,35 @@ module ActiveRecord end end + # Disconnect all connections that have been idle for at least + # +minimum_idle+ seconds. Connections currently checked out, or that were + # checked in less than +minimum_idle+ seconds ago, are unaffected. + def flush(minimum_idle = @idle_timeout) + return if minimum_idle.nil? + + idle_connections = synchronize do + @connections.select do |conn| + !conn.in_use? && conn.seconds_idle >= minimum_idle + end.each do |conn| + conn.lease + + @available.delete conn + @connections.delete conn + end + end + + idle_connections.each do |conn| + conn.disconnect! + end + end + + # Disconnect all currently idle connections. Connections currently checked + # out are unaffected. + def flush! + reap + flush(-1) + end + def num_waiting_in_queue # :nodoc: @available.num_waiting end @@ -866,11 +913,31 @@ module ActiveRecord # about the model. The model needs to pass a specification name to the handler, # in order to look up the correct connection pool. class ConnectionHandler + def self.unowned_pool_finalizer(pid_map) # :nodoc: + lambda do |_| + discard_unowned_pools(pid_map) + end + end + + def self.discard_unowned_pools(pid_map) # :nodoc: + pid_map.each do |pid, pools| + pools.values.compact.each(&:discard!) unless pid == Process.pid + end + end + def initialize # These caches are keyed by spec.name (ConnectionSpecification#name). @owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h, k| + # Discard the parent's connection pools immediately; we have no need + # of them + ConnectionHandler.discard_unowned_pools(h) + h[k] = Concurrent::Map.new(initial_capacity: 2) end + + # Backup finalizer: if the forked child never needed a pool, the above + # early discard has not occurred + ObjectSpace.define_finalizer self, ConnectionHandler.unowned_pool_finalizer(@owner_to_pool) end def connection_pool_list @@ -924,6 +991,13 @@ module ActiveRecord connection_pool_list.each(&:disconnect!) end + # Disconnects all currently idle connections. + # + # See ConnectionPool#flush! for details. + def flush_idle_connections! + connection_pool_list.each(&:flush!) + end + # Locate the connection of the nearest super class. This can be an # active or defined connection: if it is the latter, it will be # opened and set as the active connection for the class it was defined @@ -931,16 +1005,14 @@ module ActiveRecord def retrieve_connection(spec_name) #:nodoc: pool = retrieve_connection_pool(spec_name) raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool - conn = pool.connection - raise ConnectionNotEstablished, "No connection for '#{spec_name}' in connection pool" unless conn - conn + pool.connection end # Returns true if a connection that's accessible to this class has # already been opened. def connected?(spec_name) - conn = retrieve_connection_pool(spec_name) - conn && conn.connected? + pool = retrieve_connection_pool(spec_name) + pool && pool.connected? end # Remove the connection for this class. This will close the active 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 36048bee03..41553cfa83 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -356,38 +356,36 @@ module ActiveRecord # Inserts a set of fixtures into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixtures(fixtures, table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `insert_fixtures` is deprecated and will be removed in the next version of Rails. + Consider using `insert_fixtures_set` for performance improvement. + MSG return if fixtures.empty? - columns = schema_cache.columns_hash(table_name) + execute(build_fixture_sql(fixtures, table_name), "Fixtures Insert") + end - values = fixtures.map do |fixture| - fixture = fixture.stringify_keys + def insert_fixtures_set(fixture_set, tables_to_delete = []) + fixture_inserts = fixture_set.map do |table_name, fixtures| + next if fixtures.empty? - unknown_columns = fixture.keys - columns.keys - if unknown_columns.any? - raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) - end + build_fixture_sql(fixtures, table_name) + end.compact - columns.map do |name, column| - if fixture.key?(name) - type = lookup_cast_type_from_column(column) - bind = Relation::QueryAttribute.new(name, fixture[name], type) - with_yaml_fallback(bind.value_for_database) - else - Arel.sql("DEFAULT") + table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup } + total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts)) + + disable_referential_integrity do + transaction(requires_new: true) do + total_sql.each do |sql| + execute sql, "Fixtures Load" + yield if block_given? end end end - - table = Arel::Table.new(table_name) - manager = Arel::InsertManager.new - manager.into(table) - columns.each_key { |column| manager.columns << table[column] } - manager.values = manager.create_values_list(values) - execute manager.to_sql, "Fixtures Insert" end - def empty_insert_statement_value + def empty_insert_statement_value(primary_key = nil) "DEFAULT VALUES" end @@ -416,6 +414,44 @@ module ActiveRecord alias join_to_delete join_to_update private + def default_insert_value(column) + Arel.sql("DEFAULT") + end + + def build_fixture_sql(fixtures, table_name) + columns = schema_cache.columns_hash(table_name) + + values = fixtures.map do |fixture| + fixture = fixture.stringify_keys + + unknown_columns = fixture.keys - columns.keys + if unknown_columns.any? + raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) + end + + columns.map do |name, column| + if fixture.key?(name) + type = lookup_cast_type_from_column(column) + bind = Relation::QueryAttribute.new(name, fixture[name], type) + with_yaml_fallback(bind.value_for_database) + else + default_insert_value(column) + end + end + end + + table = Arel::Table.new(table_name) + manager = Arel::InsertManager.new + manager.into(table) + columns.each_key { |column| manager.columns << table[column] } + manager.values = manager.create_values_list(values) + + manager.to_sql + end + + def combine_multi_statements(total_sql) + total_sql.join(";\n") + end # Returns a subquery for the given key using the join information. def subquery_for(key, select) 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 25622e34c8..8aeb934ec2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -110,12 +110,7 @@ module ActiveRecord if @query_cache[sql].key?(binds) ActiveSupport::Notifications.instrument( "sql.active_record", - sql: sql, - binds: binds, - type_casted_binds: -> { type_casted_binds(binds) }, - name: name, - connection_id: object_id, - cached: true, + cache_notification_info(sql, name, binds) ) @query_cache[sql][binds] else @@ -125,6 +120,19 @@ module ActiveRecord end end + # Database adapters can override this method to + # provide custom cache information. + def cache_notification_info(sql, name, binds) + { + sql: sql, + binds: binds, + type_casted_binds: -> { type_casted_binds(binds) }, + name: name, + connection_id: object_id, + cached: true + } + end + # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such # queries should not be cached. def locked?(arel) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 92e46ccf9f..98b1348135 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -130,7 +130,8 @@ module ActiveRecord end def quoted_time(value) # :nodoc: - quoted_date(value).sub(/\A2000-01-01 /, "") + value = value.change(year: 2000, month: 1, day: 1) + quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "") end def quoted_binary(value) # :nodoc: 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 bd05fb8f6e..529c9d8ca6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/string/strip" - module ActiveRecord module ConnectionAdapters class AbstractAdapter @@ -17,9 +15,8 @@ module ActiveRecord end delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, to: :@conn - private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, + to: :@conn, private: true private @@ -66,7 +63,7 @@ module ActiveRecord end def visit_ForeignKeyDefinition(o) - sql = <<-SQL.strip_heredoc + sql = +<<~SQL 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)}) @@ -95,6 +92,7 @@ module ActiveRecord if options_sql = options[:options] create_sql << " #{options_sql}" end + create_sql end def column_options(o) @@ -132,7 +130,7 @@ module ActiveRecord when :cascade then "ON #{action} CASCADE" when :restrict then "ON #{action} RESTRICT" else - raise ArgumentError, <<-MSG.strip_heredoc + raise ArgumentError, <<~MSG '#{dependency}' is not supported for :on_update or :on_delete. Supported values are: :nullify, :cascade, :restrict MSG 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 be2f625d74..582ac516c7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,7 +6,7 @@ module ActiveRecord # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes class IndexDefinition # :nodoc: - attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment + attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :comment def initialize( table, name, @@ -14,6 +14,7 @@ module ActiveRecord columns = [], lengths: {}, orders: {}, + opclasses: {}, where: nil, type: nil, using: nil, @@ -23,13 +24,23 @@ module ActiveRecord @name = name @unique = unique @columns = columns - @lengths = lengths - @orders = orders + @lengths = concise_options(lengths) + @orders = concise_options(orders) + @opclasses = concise_options(opclasses) @where = where @type = type @using = using @comment = comment end + + private + def concise_options(options) + if columns.size == options.size && options.values.uniq.size == 1 + options.values.first + else + options + end + end end # Abstract representation of a column definition. Instances of this type @@ -85,6 +96,15 @@ module ActiveRecord options[:primary_key] != default_primary_key end + def validate? + options.fetch(:validate, true) + end + alias validated? validate? + + def export_name_on_schema_dump? + name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern + end + def defined_for?(to_table_ord = nil, to_table: nil, **options) if to_table_ord self.to_table == to_table_ord.to_s @@ -135,13 +155,8 @@ module ActiveRecord end end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options - private + attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options def as_options(value) value.is_a?(Hash) ? value : {} @@ -204,6 +219,7 @@ module ActiveRecord :decimal, :float, :integer, + :json, :string, :text, :time, @@ -340,8 +356,12 @@ module ActiveRecord type = type.to_sym if type options = options.dup - if @columns_hash[name] && @columns_hash[name].primary_key? - raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." + if @columns_hash[name] + if @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." + else + raise ArgumentError, "you can't define an already defined column '#{name}'." + end end index_options = options.delete(:index) @@ -481,6 +501,9 @@ module ActiveRecord # t.date # t.binary # t.boolean + # t.foreign_key + # t.json + # t.virtual # t.remove # t.remove_references # t.remove_belongs_to @@ -645,19 +668,19 @@ module ActiveRecord # Adds a foreign key. # - # t.foreign_key(:authors) + # t.foreign_key(:authors) # # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key] - def foreign_key(*args) # :nodoc: + def foreign_key(*args) @base.add_foreign_key(name, *args) end # Checks to see if a foreign key exists. # - # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors) + # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors) # # See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?] - def foreign_key_exists?(*args) # :nodoc: + def foreign_key_exists?(*args) @base.foreign_key_exists?(name, *args) 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 1926603474..622e00fffb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/hash/compact" - module ActiveRecord module ConnectionAdapters # :nodoc: class SchemaDumper < SchemaDumper # :nodoc: 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 9b7345f7c3..4702de1964 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -305,7 +305,7 @@ module ActiveRecord yield td if block_given? if options[:force] - drop_table(table_name, **options, if_exists: true) + drop_table(table_name, options.merge(if_exists: true)) end result = execute schema_creation.accept td @@ -406,7 +406,7 @@ module ActiveRecord # # Defaults to false. # - # Only supported on the MySQL adapter, ignored elsewhere. + # Only supported on the MySQL and PostgreSQL adapter, ignored elsewhere. # # ====== Add a column # @@ -600,7 +600,7 @@ module ActiveRecord # to provide these in a migration's +change+ method so it can be reverted. # In that case, +type+ and +options+ will be used by #add_column. def remove_column(table_name, column_name, type = nil, options = {}) - execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}" + execute "ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, options)}" end # Changes the column's definition according to the new options. @@ -715,7 +715,7 @@ module ActiveRecord # # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # - # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it). + # Note: MySQL only supports index order from 8.0.1 onwards (earlier versions accepted the syntax but ignored it). # # ====== Creating a partial index # @@ -738,6 +738,19 @@ module ActiveRecord # # Note: only supported by PostgreSQL and MySQL # + # ====== Creating an index with a specific operator class + # + # add_index(:developers, :name, using: 'gist', opclass: :gist_trgm_ops) + # # CREATE INDEX developers_on_name ON developers USING gist (name gist_trgm_ops) -- PostgreSQL + # + # add_index(:developers, [:name, :city], using: 'gist', opclass: { city: :gist_trgm_ops }) + # # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL + # + # add_index(:developers, [:name, :city], using: 'gist', opclass: :gist_trgm_ops) + # # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL + # + # Note: only supported by PostgreSQL + # # ====== Creating an index with a specific type # # add_index(:developers, :name, type: :fulltext) @@ -886,7 +899,7 @@ module ActiveRecord foreign_key_options = { to_table: reference_name } end foreign_key_options[:column] ||= "#{ref_name}_id" - remove_foreign_key(table_name, **foreign_key_options) + remove_foreign_key(table_name, foreign_key_options) end remove_column(table_name, "#{ref_name}_id") @@ -942,6 +955,8 @@ module ActiveRecord # 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+ + # [<tt>:validate</tt>] + # (Postgres only) Specify whether or not the constraint should be validated. Defaults to +true+. def add_foreign_key(from_table, to_table, options = {}) return unless supports_foreign_keys? @@ -965,11 +980,18 @@ module ActiveRecord # # remove_foreign_key :accounts, column: :owner_id # + # Removes the foreign key on +accounts.owner_id+. + # + # remove_foreign_key :accounts, to_table: :owners + # # Removes the foreign key named +special_fk_name+ on the +accounts+ table. # # remove_foreign_key :accounts, name: :special_fk_name # - # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key + # with an addition of + # [<tt>:to_table</tt>] + # The name of the table that contains the referenced primary key. def remove_foreign_key(from_table, options_or_to_table = {}) return unless supports_foreign_keys? @@ -1025,8 +1047,8 @@ module ActiveRecord sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i) - versions = ActiveRecord::Migrator.migration_files(migrations_paths).map do |file| - ActiveRecord::Migrator.parse_migration_filename(file).first.to_i + versions = migration_context.migration_files.map do |file| + migration_context.parse_migration_filename(file).first.to_i end unless migrated.include?(version) @@ -1038,13 +1060,7 @@ module ActiveRecord if (duplicate = inserting.detect { |v| inserting.count(v) > 1 }) raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict." end - if supports_multi_insert? - execute insert_versions_sql(inserting) - else - inserting.each do |v| - execute insert_versions_sql(v) - end - end + execute insert_versions_sql(inserting) end end @@ -1120,7 +1136,7 @@ module ActiveRecord def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc: column_names = index_column_names(column_name) - options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) + options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclass) index_type = options[:type].to_s if options.key?(:type) index_type ||= options[:unique] ? "UNIQUE" : "" @@ -1173,20 +1189,22 @@ module ActiveRecord end def add_index_sort_order(quoted_columns, **options) - if order = options[:order] - case order - when Hash - order = order.symbolize_keys - quoted_columns.each { |name, column| column << " #{order[name].upcase}" if order[name].present? } - when String - quoted_columns.each { |name, column| column << " #{order.upcase}" if order.present? } - end + orders = options_for_index_columns(options[:order]) + quoted_columns.each do |name, column| + column << " #{orders[name].upcase}" if orders[name].present? end + end - quoted_columns + def options_for_index_columns(options) + if options.is_a?(Hash) + options.symbolize_keys + else + Hash.new { |hash, column| hash[column] = options } + end end - # Overridden by the MySQL adapter for supporting index lengths + # Overridden by the MySQL adapter for supporting index lengths and by + # the PostgreSQL adapter for supporting operator classes. def add_options_for_index_columns(quoted_columns, **options) if supports_index_sort_order? quoted_columns = add_index_sort_order(quoted_columns, options) @@ -1340,6 +1358,20 @@ module ActiveRecord options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty? end + def add_column_for_alter(table_name, column_name, type, options = {}) + td = create_table_definition(table_name) + cd = td.new_column_definition(column_name, type, options) + schema_creation.accept(AddColumnDefinition.new(cd)) + end + + def remove_column_for_alter(table_name, column_name, type = nil, options = {}) + "DROP COLUMN #{quote_column_name(column_name)}" + end + + def remove_columns_for_alter(table_name, *column_names) + column_names.map { |column_name| remove_column_for_alter(table_name, column_name) } + end + def insert_versions_sql(versions) sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index d9ac8db6a8..b59df2fff7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -17,11 +17,19 @@ module ActiveRecord end def committed? - @state == :committed + @state == :committed || @state == :fully_committed + end + + def fully_committed? + @state == :fully_committed end def rolledback? - @state == :rolledback + @state == :rolledback || @state == :fully_rolledback + end + + def fully_rolledback? + @state == :fully_rolledback end def fully_completed? @@ -55,10 +63,19 @@ module ActiveRecord @state = :rolledback end + def full_rollback! + @children.each { |c| c.rollback! } + @state = :fully_rolledback + end + def commit! @state = :committed end + def full_commit! + @state = :fully_committed + end + def nullify! @state = nil end @@ -75,7 +92,6 @@ module ActiveRecord class Transaction #:nodoc: attr_reader :connection, :state, :records, :savepoint_name - attr_writer :joinable def initialize(connection, options, run_commit_callbacks: false) @connection = connection @@ -89,10 +105,6 @@ module ActiveRecord records << record end - def rollback - @state.rollback! - end - def rollback_records ite = records.uniq while record = ite.shift @@ -104,10 +116,6 @@ module ActiveRecord end end - def commit - @state.commit! - end - def before_commit_records records.uniq.each(&:before_committed!) if @run_commit_callbacks end @@ -146,12 +154,12 @@ module ActiveRecord def rollback connection.rollback_to_savepoint(savepoint_name) - super + @state.rollback! end def commit connection.release_savepoint(savepoint_name) - super + @state.commit! end def full_rollback?; false; end @@ -169,12 +177,12 @@ module ActiveRecord def rollback connection.rollback_db_transaction - super + @state.full_rollback! end def commit connection.commit_db_transaction - super + @state.full_commit! 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 345983a655..a4748dbeda 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -5,6 +5,7 @@ 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 "active_support/concurrency/load_interlock_aware_monitor" require "arel/collectors/bind" require "arel/collectors/composite" require "arel/collectors/sql_string" @@ -80,7 +81,9 @@ module ActiveRecord alias :in_use? :owner def self.type_cast_config_to_integer(config) - if config =~ SIMPLE_INT + if config.is_a?(Integer) + config + elsif SIMPLE_INT.match?(config) config.to_i else config @@ -104,10 +107,11 @@ module ActiveRecord @logger = logger @config = config @pool = nil + @idle_since = Concurrent.monotonic_time @schema_cache = SchemaCache.new self @quoted_column_names, @quoted_table_names = {}, {} @visitor = arel_visitor - @lock = Monitor.new + @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true @@ -117,6 +121,14 @@ module ActiveRecord end end + def migrations_paths # :nodoc: + @config[:migrations_paths] || Migrator.migrations_paths + end + + def migration_context # :nodoc: + MigrationContext.new(migrations_paths) + end + class Version include Comparable @@ -127,6 +139,10 @@ module ActiveRecord def <=>(version_string) @version <=> version_string.split(".").map(&:to_i) end + + def to_s + @version.join(".") + end end def valid_type?(type) # :nodoc: @@ -163,6 +179,7 @@ module ActiveRecord "Current thread: #{Thread.current}." end + @idle_since = Concurrent.monotonic_time @owner = nil else raise ActiveRecordError, "Cannot expire connection, it is not currently leased." @@ -182,6 +199,12 @@ module ActiveRecord end end + # Seconds since this connection was returned to the pool + def seconds_idle # :nodoc: + return 0 if in_use? + Concurrent.monotonic_time - @idle_since + end + def unprepared_statement old_prepared_statements, @prepared_statements = @prepared_statements, false yield @@ -263,6 +286,11 @@ module ActiveRecord false end + # Does this adapter support creating invalid constraints? + def supports_validate_constraints? + false + end + # Does this adapter support creating foreign key constraints # in the same statement as creating the table? def supports_foreign_keys_in_create? @@ -298,12 +326,18 @@ module ActiveRecord def supports_multi_insert? true end + deprecate :supports_multi_insert? # Does this adapter support virtual columns? def supports_virtual_columns? false end + # Does this adapter support foreign/external tables? + def supports_foreign_tables? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -366,6 +400,19 @@ module ActiveRecord reset_transaction end + # Immediately forget this connection ever existed. Unlike disconnect!, + # this will not communicate with the server. + # + # After calling this method, the behavior of all other methods becomes + # undefined. This is called internally just before a forked process gets + # rid of a connection that belonged to its parent. + def discard! + # This should be overridden by concrete adapters. + # + # Prevent @connection's finalizer from touching the socket, or + # otherwise communicating with its server, when it is collected. + end + # Reset the state of this connection, directing the DBMS to clear # transactions and other connection-related server-side state. Usually a # database-dependent operation. @@ -510,12 +557,7 @@ module ActiveRecord end def extract_limit(sql_type) - case sql_type - when /^bigint/i - 8 - when /\((.*)\)/ - $1.to_i - end + $1.to_i if sql_type =~ /\((.*)\)/ end def translate_exception_class(e, sql) 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 bfec6fb784..9de8242a58 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -11,8 +11,6 @@ require "active_record/connection_adapters/mysql/schema_dumper" require "active_record/connection_adapters/mysql/schema_statements" require "active_record/connection_adapters/mysql/type_metadata" -require "active_support/core_ext/string/strip" - module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter @@ -44,9 +42,9 @@ module ActiveRecord json: { name: "json" }, } - class StatementPool < ConnectionAdapters::StatementPool + class StatementPool < ConnectionAdapters::StatementPool # :nodoc: private def dealloc(stmt) - stmt[:stmt].close + stmt.close end end @@ -117,11 +115,11 @@ module ActiveRecord end def get_advisory_lock(lock_name, timeout = 0) # :nodoc: - query_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1 + query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1 end def release_advisory_lock(lock_name) # :nodoc: - query_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1 + query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1 end def native_database_types @@ -225,7 +223,7 @@ module ActiveRecord end end - def empty_insert_statement_value + def empty_insert_statement_value(primary_key = nil) "VALUES ()" end @@ -249,7 +247,7 @@ module ActiveRecord # create_database 'matt_development', charset: :big5 def create_database(name, options = {}) if options[:collation] - execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}" + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}" else execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}" end @@ -284,7 +282,7 @@ module ActiveRecord def table_comment(table_name) # :nodoc: scope = quoted_scope(table_name) - query_value(<<-SQL.strip_heredoc, "SCHEMA").presence + query_value(<<~SQL, "SCHEMA").presence SELECT table_comment FROM information_schema.tables WHERE table_schema = #{scope[:schema]} @@ -292,14 +290,10 @@ module ActiveRecord SQL end - def create_table(table_name, **options) #:nodoc: - super(table_name, options: "ENGINE=InnoDB", **options) - end - def bulk_change_table(table_name, operations) #:nodoc: sqls = operations.flat_map do |command, args| table, arguments = args.shift, args - method = :"#{command}_sql" + method = :"#{command}_for_alter" if respond_to?(method, true) send(method, table, *arguments) @@ -372,11 +366,11 @@ module ActiveRecord end def change_column(table_name, column_name, type, options = {}) #:nodoc: - execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") + execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, options)}") end def rename_column(table_name, column_name, new_column_name) #:nodoc: - execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}") + execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_for_alter(table_name, column_name, new_column_name)}") rename_column_indexes(table_name, column_name, new_column_name) end @@ -396,19 +390,20 @@ module ActiveRecord scope = quoted_scope(table_name) - fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA") + fk_info = exec_query(<<~SQL, "SCHEMA") 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', rc.update_rule AS 'on_update', rc.delete_rule AS 'on_delete' - FROM information_schema.key_column_usage fk - JOIN information_schema.referential_constraints rc + FROM information_schema.referential_constraints rc + JOIN information_schema.key_column_usage fk USING (constraint_schema, constraint_name) WHERE fk.referenced_column_name IS NOT NULL AND fk.table_schema = #{scope[:schema]} AND fk.table_name = #{scope[:name]} + AND rc.constraint_schema = #{scope[:schema]} AND rc.table_name = #{scope[:name]} SQL @@ -483,7 +478,7 @@ module ActiveRecord scope = quoted_scope(table_name) - query_values(<<-SQL.strip_heredoc, "SCHEMA") + query_values(<<~SQL, "SCHEMA") SELECT column_name FROM information_schema.key_column_usage WHERE constraint_name = 'PRIMARY' @@ -518,7 +513,7 @@ module ActiveRecord s.gsub(/\s+(?:ASC|DESC)\b/i, "") }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } - [super, *order_columns].join(", ") + (order_columns << super).join(", ") end def strict_mode? @@ -529,23 +524,38 @@ module ActiveRecord index.using == :btree || super end - def insert_fixtures(*) - without_sql_mode("NO_AUTO_VALUE_ON_ZERO") { super } + def insert_fixtures_set(fixture_set, tables_to_delete = []) + with_multi_statements do + super { discard_remaining_results } + end end private + def combine_multi_statements(total_sql) + total_sql.each_with_object([]) do |sql, total_sql_chunks| + previous_packet = total_sql_chunks.last + sql << ";\n" + if max_allowed_packet_reached?(sql, previous_packet) || total_sql_chunks.empty? + total_sql_chunks << sql + else + previous_packet << sql + end + end + end - def without_sql_mode(mode) - result = execute("SELECT @@SESSION.sql_mode") - current_mode = result.first[0] - return yield unless current_mode.include?(mode) + def max_allowed_packet_reached?(current_packet, previous_packet) + if current_packet.bytesize > max_allowed_packet + raise ActiveRecordError, "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable." + elsif previous_packet.nil? + false + else + (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet + end + end - sql_mode = "REPLACE(@@sql_mode, '#{mode}', '')" - execute("SET @@SESSION.sql_mode = #{sql_mode}") - yield - ensure - sql_mode = "CONCAT(@@sql_mode, ',#{mode}')" - execute("SET @@SESSION.sql_mode = #{sql_mode}") + def max_allowed_packet + bytes_margin = 2 + @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin) end def initialize_type_map(m = type_map) @@ -605,29 +615,11 @@ module ActiveRecord end end - def add_index_length(quoted_columns, **options) - if length = options[:length] - case length - when Hash - length = length.symbolize_keys - quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? } - when Integer - quoted_columns.each { |name, column| column << "(#{length})" } - end - end - - quoted_columns - end - - def add_options_for_index_columns(quoted_columns, **options) - quoted_columns = add_index_length(quoted_columns, options) - super - end - # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html ER_DUP_ENTRY = 1062 ER_NOT_NULL_VIOLATION = 1048 ER_DO_NOT_HAVE_DEFAULT = 1364 + ER_ROW_IS_REFERENCED_2 = 1451 ER_NO_REFERENCED_ROW_2 = 1452 ER_DATA_TOO_LONG = 1406 ER_OUT_OF_RANGE = 1264 @@ -635,12 +627,14 @@ module ActiveRecord ER_CANNOT_ADD_FOREIGN = 1215 ER_CANNOT_CREATE_TABLE = 1005 ER_LOCK_WAIT_TIMEOUT = 1205 + ER_QUERY_INTERRUPTED = 1317 + ER_QUERY_TIMEOUT = 3024 def translate_exception(exception, message) case error_number(exception) when ER_DUP_ENTRY RecordNotUnique.new(message) - when ER_NO_REFERENCED_ROW_2 + when ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 InvalidForeignKey.new(message) when ER_CANNOT_ADD_FOREIGN mismatched_foreign_key(message) @@ -659,19 +653,17 @@ module ActiveRecord when ER_LOCK_DEADLOCK Deadlocked.new(message) when ER_LOCK_WAIT_TIMEOUT - TransactionTimeout.new(message) + LockWaitTimeout.new(message) + when ER_QUERY_TIMEOUT + StatementTimeout.new(message) + when ER_QUERY_INTERRUPTED + QueryCanceled.new(message) else super end end - def add_column_sql(table_name, column_name, type, options = {}) - td = create_table_definition(table_name) - cd = td.new_column_definition(column_name, type, options) - schema_creation.accept(AddColumnDefinition.new(cd)) - end - - def change_column_sql(table_name, column_name, type, options = {}) + def change_column_for_alter(table_name, column_name, type, options = {}) column = column_for(table_name, column_name) type ||= column.sql_type @@ -692,7 +684,7 @@ module ActiveRecord schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end - def rename_column_sql(table_name, column_name, new_column_name) + def rename_column_for_alter(table_name, column_name, new_column_name) column = column_for(table_name, column_name) options = { default: column.default, @@ -706,31 +698,23 @@ module ActiveRecord schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end - def remove_column_sql(table_name, column_name, type = nil, options = {}) - "DROP #{quote_column_name(column_name)}" - end - - def remove_columns_sql(table_name, *column_names) - column_names.map { |column_name| remove_column_sql(table_name, column_name) } - end - - def add_index_sql(table_name, column_name, options = {}) + def add_index_for_alter(table_name, column_name, options = {}) index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) index_algorithm[0, 0] = ", " if index_algorithm.present? "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}" end - def remove_index_sql(table_name, options = {}) + def remove_index_for_alter(table_name, options = {}) index_name = index_name_for_remove(table_name, options) "DROP INDEX #{quote_column_name(index_name)}" end - 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)] + def add_timestamps_for_alter(table_name, options = {}) + [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)] end - def remove_timestamps_sql(table_name, options = {}) - [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] + def remove_timestamps_for_alter(table_name, options = {}) + [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)] end # MySQL is too stupid to create a temporary table for use subquery, so we have @@ -743,7 +727,8 @@ module ActiveRecord # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' subselect.distinct unless select.limit || select.offset || select.orders.any? - Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key.name)) + key_name = quote_column_name(key.name) + Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name)) end def supports_rename_index? diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 508132accb..204691006c 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -57,9 +57,7 @@ module ActiveRecord private - def uri - @uri - end + attr_reader :uri def uri_parser @uri_parser ||= URI::Parser.new @@ -156,7 +154,6 @@ module ActiveRecord env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url")) end - config.reject! { |k, v| v.is_a?(Hash) && !(v.key?("adapter") || v.key?("url")) } config.merge! env_config if env_config config.each do |key, value| diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index a058a72872..d89eeb7f54 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -11,7 +11,7 @@ module ActiveRecord else super end - @connection.next_result while @connection.more_results? + discard_remaining_results result end @@ -50,11 +50,54 @@ module ActiveRecord alias :exec_update :exec_delete private + def default_insert_value(column) + Arel.sql("DEFAULT") unless column.auto_increment? + end def last_inserted_id(result) @connection.last_id end + def discard_remaining_results + @connection.abandon_results! + end + + def supports_set_server_option? + @connection.respond_to?(:set_server_option) + end + + def multi_statements_enabled?(flags) + if flags.is_a?(Array) + flags.include?("MULTI_STATEMENTS") + else + (flags & Mysql2::Client::MULTI_STATEMENTS) != 0 + end + end + + def with_multi_statements + previous_flags = @config[:flags] + + unless multi_statements_enabled?(previous_flags) + if supports_set_server_option? + @connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_ON) + else + @config[:flags] = Mysql2::Client::MULTI_STATEMENTS + reconnect! + end + end + + yield + ensure + unless multi_statements_enabled?(previous_flags) + if supports_set_server_option? + @connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF) + else + @config[:flags] = previous_flags + reconnect! + end + end + end + def exec_stmt_and_free(sql, name, binds, cache_stmt: false) # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been # made since we established the connection @@ -64,10 +107,7 @@ module ActiveRecord log(sql, name, binds, type_casted_binds) do if cache_stmt - cache = @statements[sql] ||= { - stmt: @connection.prepare(sql) - } - stmt = cache[:stmt] + stmt = @statements[sql] ||= @connection.prepare(sql) else stmt = @connection.prepare(sql) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index 75377693c6..c9ea653b77 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -4,8 +4,7 @@ module ActiveRecord module ConnectionAdapters module MySQL class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: - delegate :add_sql_comment!, :mariadb?, to: :@conn - private :add_sql_comment!, :mariadb? + delegate :add_sql_comment!, :mariadb?, to: :@conn, private: true private diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index da25e4863c..2ed4ad16ae 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -32,10 +32,6 @@ module ActiveRecord args.each { |name| column(name, :longtext, options) } end - def json(*args, **options) - args.each { |name| column(name, :json, options) } - end - def unsigned_integer(*args, **options) args.each { |name| column(name, :unsigned_integer, options) } end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index a15c7d1787..1cf210d85b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -22,23 +22,26 @@ module ActiveRecord index_using = mysql_index_type end - indexes << IndexDefinition.new( + indexes << [ row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, + [], + lengths: {}, + orders: {}, type: index_type, using: index_using, comment: row[:Index_comment].presence - ) + ] end - indexes.last.columns << row[:Column_name] - indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] - indexes.last.orders.merge!(row[:Column_name] => :desc) if row[:Collation] == "D" + indexes.last[-2] << row[:Column_name] + indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part] + indexes.last[-1][:orders].merge!(row[:Column_name] => :desc) if row[:Collation] == "D" end end - indexes + indexes.map { |index| IndexDefinition.new(*index) } end def remove_column(table_name, column_name, type = nil, options = {}) @@ -77,8 +80,8 @@ module ActiveRecord def new_column_from_field(table_name, field) type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\(\))?\z/i.match?(field[:Default]) - default, default_function = nil, "CURRENT_TIMESTAMP" + if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(field[:Default]) + default, default_function = nil, field[:Default] else default, default_function = field[:Default], nil end @@ -103,6 +106,18 @@ module ActiveRecord super unless specifier == "RESTRICT" end + def add_index_length(quoted_columns, **options) + lengths = options_for_index_columns(options[:length]) + quoted_columns.each do |name, column| + column << "(#{lengths[name]})" if lengths[name].present? + end + end + + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_length(quoted_columns, options) + super + end + def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 8de582fee1..544d720428 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -3,9 +3,8 @@ require "active_record/connection_adapters/abstract_mysql_adapter" require "active_record/connection_adapters/mysql/database_statements" -gem "mysql2", ">= 0.3.18", "< 0.5" +gem "mysql2", ">= 0.4.4", "< 0.6.0" require "mysql2" -raise "mysql2 0.4.3 is not supported. Please upgrade to 0.4.4+" if Mysql2::VERSION == "0.4.3" module ActiveRecord module ConnectionHandling # :nodoc: @@ -105,6 +104,11 @@ module ActiveRecord @connection.close end + def discard! # :nodoc: + @connection.automatic_close = false + @connection = nil + end + private def connect @@ -113,7 +117,7 @@ module ActiveRecord end def configure_connection - @connection.query_options.merge!(as: :array) + @connection.query_options[:as] = :array super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 469ef3f5a0..3ccc7271ab 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -20,10 +20,9 @@ module ActiveRecord end end - protected + private attr_reader :max_identifier_length - private def sequence_name_from_parts(table_name, column_name, suffix) over_length = [table_name, column_name, suffix].map(&:length).sum + 2 - max_identifier_length diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 542ca75d3e..247a25054e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -5,6 +5,7 @@ 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" 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 d6852082ac..26abeea7ed 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -66,6 +66,10 @@ module ActiveRecord deserialize(raw_old_value) != new_value end + def force_equality?(value) + value.is_a?(::Array) + end + private def type_cast_array(value, method) 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 587e95d192..e9a79526f9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -43,10 +43,7 @@ module ActiveRecord /\A[0-9A-F]*\Z/i.match?(value) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :value end 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 new file mode 100644 index 0000000000..24a1daa95a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Date < Type::Date # :nodoc: + def cast_value(value) + case value + when "infinity" then ::Float::INFINITY + when "-infinity" then -::Float::INFINITY + when / BC$/ + astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) + super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) + else + super + end + end + end + end + end + end +end 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 879dba7afd..e7d33855c4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb @@ -6,7 +6,7 @@ module ActiveRecord module OID # :nodoc: class Decimal < Type::Decimal # :nodoc: def infinity(options = {}) - BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) + BigDecimal("Infinity") * (options[:negative] ? -1 : 1) 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 a89aa5ea09..d85f9ab3ef 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -53,6 +53,10 @@ module ActiveRecord ::Range.new(new_begin, new_end, value.exclude_end?) end + def force_equality?(value) + value.is_a?(::Range) + end + private def type_cast_single(value) @@ -60,7 +64,7 @@ module ActiveRecord end def type_cast_single_for_database(value) - infinity?(value) ? "" : @subtype.serialize(value) + infinity?(value) ? value : @subtype.serialize(value) end def extract_bounds(value) 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 231278c184..79351bc3a4 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 @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/array/extract" + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -16,12 +18,12 @@ module ActiveRecord def run(records) nodes = records.reject { |row| @store.key? row["oid"].to_i } - mapped, nodes = nodes.partition { |row| @store.key? row["typname"] } - ranges, nodes = nodes.partition { |row| row["typtype"] == "r".freeze } - enums, nodes = nodes.partition { |row| row["typtype"] == "e".freeze } - domains, nodes = nodes.partition { |row| row["typtype"] == "d".freeze } - arrays, nodes = nodes.partition { |row| row["typinput"] == "array_in".freeze } - composites, nodes = nodes.partition { |row| row["typelem"].to_i != 0 } + mapped = nodes.extract! { |row| @store.key? row["typname"] } + ranges = nodes.extract! { |row| row["typtype"] == "r".freeze } + enums = nodes.extract! { |row| row["typtype"] == "e".freeze } + domains = nodes.extract! { |row| row["typtype"] == "d".freeze } + arrays = nodes.extract! { |row| row["typinput"] == "array_in".freeze } + composites = nodes.extract! { |row| row["typelem"].to_i != 0 } mapped.each { |row| register_mapped_type(row) } enums.each { |row| register_enum_type(row) } diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 9fdeab06c1..e75202b0be 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -138,7 +138,7 @@ module ActiveRecord end def encode_range(range) - "[#{type_cast(range.first)},#{type_cast(range.last)}#{range.exclude_end? ? ')' : ']'}" + "[#{type_cast_range_value(range.first)},#{type_cast_range_value(range.last)}#{range.exclude_end? ? ')' : ']'}" end def determine_encoding_of_strings_in_array(value) @@ -154,6 +154,14 @@ module ActiveRecord else _type_cast(values) end end + + def type_cast_range_value(value) + infinity?(value) ? "" : type_cast(value) + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb index 59f661da25..8e381a92cf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb @@ -5,6 +5,18 @@ module ActiveRecord module PostgreSQL class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: private + def visit_AlterTable(o) + super << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ") + end + + def visit_AddForeignKey(o) + super.dup.tap { |sql| sql << " NOT VALID" unless o.validate? } + end + + def visit_ValidateConstraint(name) + "VALIDATE CONSTRAINT #{quote_column_name(name)}" + end + def add_column_options!(sql, options) if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" 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 75622eb304..206b855a18 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -13,10 +13,10 @@ module ActiveRecord # t.timestamps # end # - # By default, this will use the +gen_random_uuid()+ function from the + # By default, this will use the <tt>gen_random_uuid()</tt> function from the # +pgcrypto+ extension. As that extension is only available in # PostgreSQL 9.4+, for earlier versions an explicit default can be set - # to use +uuid_generate_v4()+ from the +uuid-ossp+ extension instead: + # to use <tt>uuid_generate_v4()</tt> from the +uuid-ossp+ extension instead: # # create_table :stuffs, id: false do |t| # t.primary_key :id, :uuid, default: "uuid_generate_v4()" @@ -95,10 +95,6 @@ module ActiveRecord args.each { |name| column(name, :int8range, options) } end - def json(*args, **options) - args.each { |name| column(name, :json, options) } - end - def jsonb(*args, **options) args.each { |name| column(name, :jsonb, options) } end @@ -192,6 +188,19 @@ module ActiveRecord class Table < ActiveRecord::ConnectionAdapters::Table include ColumnMethods end + + class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable + attr_reader :constraint_validations + + def initialize(td) + super + @constraint_validations = [] + end + + def validate_constraint(name) + @constraint_validations << name + end + end end end end 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 846e721983..00da7690a2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -38,7 +38,7 @@ module ActiveRecord " TABLESPACE = \"#{value}\"" when :connection_limit " CONNECTION LIMIT = #{value}" - else + else "" end end @@ -87,10 +87,7 @@ module ActiveRecord result = query(<<-SQL, "SCHEMA") SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, - pg_catalog.obj_description(i.oid, 'pg_class') AS comment, - (SELECT COUNT(*) FROM pg_opclass o - JOIN (SELECT unnest(string_to_array(d.indclass::text, ' '))::int oid) c - ON o.oid = c.oid WHERE o.opcdefault = 'f') + pg_catalog.obj_description(i.oid, 'pg_class') AS comment FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid INNER JOIN pg_class i ON d.indexrelid = i.oid @@ -109,24 +106,32 @@ module ActiveRecord inddef = row[3] oid = row[4] comment = row[5] - opclass = row[6] - using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten + using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten + + orders = {} + opclasses = {} - if indkey.include?(0) || opclass > 0 + if indkey.include?(0) columns = expressions else - columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact + columns = Hash[query(<<~SQL, "SCHEMA")].values_at(*indkey).compact SELECT a.attnum, a.attname FROM pg_attribute a WHERE a.attrelid = #{oid} AND a.attnum IN (#{indkey.join(",")}) SQL - # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - orders = Hash[ - expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] } - ] + # add info on sort order (only desc order is explicitly specified, asc is the default) + # and non-default opclasses + expressions.scan(/(?<column>\w+)\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls| + opclasses[column] = opclass.to_sym if opclass + if nulls + orders[column] = [desc, nulls].compact.join(" ") + else + orders[column] = :desc if desc + end + end end IndexDefinition.new( @@ -135,6 +140,7 @@ module ActiveRecord unique, columns, orders: orders, + opclasses: opclasses, where: where, using: using.to_sym, comment: comment.presence @@ -152,7 +158,7 @@ module ActiveRecord def table_comment(table_name) # :nodoc: scope = quoted_scope(table_name, type: "BASE TABLE") if scope[:name] - query_value(<<-SQL.strip_heredoc, "SCHEMA") + query_value(<<~SQL, "SCHEMA") SELECT pg_catalog.obj_description(c.oid, 'pg_class') FROM pg_catalog.pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace @@ -347,7 +353,7 @@ module ActiveRecord end def primary_keys(table_name) # :nodoc: - query_values(<<-SQL.strip_heredoc, "SCHEMA") + query_values(<<~SQL, "SCHEMA") SELECT a.attname FROM ( SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx @@ -362,6 +368,31 @@ module ActiveRecord SQL end + def bulk_change_table(table_name, operations) + sql_fragments = [] + non_combinable_operations = [] + + operations.each do |command, args| + table, arguments = args.shift, args + method = :"#{command}_for_alter" + + if respond_to?(method, true) + sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) } + sql_fragments << sqls + non_combinable_operations.concat(procs) + else + execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty? + non_combinable_operations.each(&:call) + sql_fragments = [] + non_combinable_operations = [] + send(command, table, *arguments) + end + end + + execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty? + non_combinable_operations.each(&:call) + end + # Renames a table. # Also renames a table's primary key sequence if the sequence name exists and # matches the Active Record default. @@ -392,50 +423,23 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! - quoted_table_name = quote_table_name(table_name) - quoted_column_name = quote_column_name(column_name) - sql_type = type_to_sql(type, options) - sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup - if options[:collation] - sql << " COLLATE \"#{options[:collation]}\"" - end - if options[:using] - sql << " USING #{options[:using]}" - elsif options[:cast_as] - cast_as_type = type_to_sql(options[:cast_as], options) - sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" - end - execute sql - - change_column_default(table_name, column_name, options[:default]) if options.key?(:default) - change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) - change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) + sqls, procs = Array(change_column_for_alter(table_name, column_name, type, options)).partition { |v| v.is_a?(String) } + execute "ALTER TABLE #{quote_table_name(table_name)} #{sqls.join(", ")}" + procs.each(&:call) end # Changes the default value of a table column. def change_column_default(table_name, column_name, default_or_changes) # :nodoc: - clear_cache! - column = column_for(table_name, column_name) - return unless column - - default = extract_new_default_value(default_or_changes) - 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 + execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}" end def change_column_null(table_name, column_name, null, default = nil) #:nodoc: 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_expression(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") + execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_null_for_alter(table_name, column_name, null, default)}" end # Adds comment for given table column or drops it if +comment+ is a +nil+ @@ -458,8 +462,8 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) - execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}").tap do + index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) + execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}").tap do execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment end end @@ -498,8 +502,8 @@ module ActiveRecord def foreign_keys(table_name) scope = quoted_scope(table_name) - fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA") - 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 + fk_info = exec_query(<<~SQL, "SCHEMA") + 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, c.convalidated AS valid FROM pg_constraint c JOIN pg_class t1 ON c.conrelid = t1.oid JOIN pg_class t2 ON c.confrelid = t2.oid @@ -521,11 +525,20 @@ module ActiveRecord options[:on_delete] = extract_foreign_key_action(row["on_delete"]) options[:on_update] = extract_foreign_key_action(row["on_update"]) + options[:validate] = row["valid"] ForeignKeyDefinition.new(table_name, row["to_table"], options) end end + def foreign_tables + query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA") + end + + def foreign_table_exists?(table_name) + query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present? + end + # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc: sql = \ @@ -570,7 +583,7 @@ module ActiveRecord .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "") }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } - [super, *order_columns].join(", ") + (order_columns << super).join(", ") end def update_table_definition(table_name, base) # :nodoc: @@ -581,6 +594,43 @@ module ActiveRecord PostgreSQL::SchemaDumper.create(self, options) end + # Validates the given constraint. + # + # Validates the constraint named +constraint_name+ on +accounts+. + # + # validate_constraint :accounts, :constraint_name + def validate_constraint(table_name, constraint_name) + return unless supports_validate_constraints? + + at = create_alter_table table_name + at.validate_constraint constraint_name + + execute schema_creation.accept(at) + end + + # Validates the given foreign key. + # + # Validates the foreign key on +accounts.branch_id+. + # + # validate_foreign_key :accounts, :branches + # + # Validates the foreign key on +accounts.owner_id+. + # + # validate_foreign_key :accounts, column: :owner_id + # + # Validates the foreign key named +special_fk_name+ on the +accounts+ table. + # + # validate_foreign_key :accounts, name: :special_fk_name + # + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. + def validate_foreign_key(from_table, options_or_to_table = {}) + return unless supports_validate_constraints? + + fk_name_to_validate = foreign_key_for!(from_table, options_or_to_table).name + + validate_constraint from_table, fk_name_to_validate + end + private def schema_creation PostgreSQL::SchemaCreation.new(self) @@ -590,6 +640,10 @@ module ActiveRecord PostgreSQL::TableDefinition.new(*args) end + def create_alter_table(name) + PostgreSQL::AlterTable.new create_table_definition(name) + end + def new_column_from_field(table_name, field) column_name, type, default, notnull, oid, fmod, collation, comment = field type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i) @@ -629,9 +683,79 @@ module ActiveRecord end end + def change_column_sql(table_name, column_name, type, options = {}) + quoted_column_name = quote_column_name(column_name) + sql_type = type_to_sql(type, options) + sql = "ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + if options[:using] + sql << " USING #{options[:using]}" + elsif options[:cast_as] + cast_as_type = type_to_sql(options[:cast_as], options) + sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" + end + + sql + end + + def add_column_for_alter(table_name, column_name, type, options = {}) + return super unless options.key?(:comment) + [super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }] + end + + def change_column_for_alter(table_name, column_name, type, options = {}) + sqls = [change_column_sql(table_name, column_name, type, options)] + sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default) + sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment) + sqls + end + + # Changes the default value of a table column. + def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc: + column = column_for(table_name, column_name) + return unless column + + default = extract_new_default_value(default_or_changes) + alter_column_query = "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". + alter_column_query % "DROP DEFAULT" + else + alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}" + end + end + + def change_column_null_for_alter(table_name, column_name, null, default = nil) #:nodoc: + "ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL" + end + + def add_timestamps_for_alter(table_name, options = {}) + [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)] + end + + def remove_timestamps_for_alter(table_name, options = {}) + [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)] + end + + def add_index_opclass(quoted_columns, **options) + opclasses = options_for_index_columns(options[:opclass]) + quoted_columns.each do |name, column| + column << " #{opclasses[name]}" if opclasses[name].present? + end + end + + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_opclass(quoted_columns, options) + super + end + def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) - scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view + scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup sql << " WHERE n.nspname = #{scope[:schema]}" @@ -645,9 +769,11 @@ module ActiveRecord type = \ case type when "BASE TABLE" - "'r'" + "'r','p'" when "VIEW" "'v','m'" + when "FOREIGN TABLE" + "'f'" end scope = {} scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index b252a76caa..ffd3be26b0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module ActiveRecord + # :stopdoc: module ConnectionAdapters class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) undef to_yaml if method_defined?(:to_yaml) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 46863c41ab..fdf6f75108 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility -gem "pg", "~> 0.18" +gem "pg", ">= 0.18", "< 2.0" require "pg" require "active_record/connection_adapters/abstract_adapter" @@ -122,6 +122,10 @@ module ActiveRecord include PostgreSQL::SchemaStatements include PostgreSQL::DatabaseStatements + def supports_bulk_alter? + true + end + def supports_index_sort_order? true end @@ -142,6 +146,10 @@ module ActiveRecord true end + def supports_validate_constraints? + true + end + def supports_views? true end @@ -166,7 +174,7 @@ module ActiveRecord { concurrently: "CONCURRENTLY" } end - class StatementPool < ConnectionAdapters::StatementPool + class StatementPool < ConnectionAdapters::StatementPool # :nodoc: def initialize(connection, max) super(max) @connection = connection @@ -182,7 +190,6 @@ module ActiveRecord end private - def dealloc(key) @connection.query "DEALLOCATE #{key}" if connection_active? rescue PG::Error @@ -273,6 +280,11 @@ module ActiveRecord end end + def discard! # :nodoc: + @connection.socket_io.reopen(IO::NULL) rescue nil + @connection = nil + end + def native_database_types #:nodoc: NATIVE_DATABASE_TYPES end @@ -306,6 +318,10 @@ module ActiveRecord postgresql_version >= 90300 end + def supports_foreign_tables? + postgresql_version >= 90300 + end + def supports_pgcrypto_uuid? postgresql_version >= 90400 end @@ -382,7 +398,6 @@ module ActiveRecord end private - # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html VALUE_LIMIT_VIOLATION = "22001" NUMERIC_VALUE_OUT_OF_RANGE = "22003" @@ -392,6 +407,7 @@ module ActiveRecord SERIALIZATION_FAILURE = "40001" DEADLOCK_DETECTED = "40P01" LOCK_NOT_AVAILABLE = "55P03" + QUERY_CANCELED = "57014" def translate_exception(exception, message) return exception unless exception.respond_to?(:result) @@ -412,7 +428,9 @@ module ActiveRecord when DEADLOCK_DETECTED Deadlocked.new(message) when LOCK_NOT_AVAILABLE - TransactionTimeout.new(message) + LockWaitTimeout.new(message) + when QUERY_CANCELED + QueryCanceled.new(message) else super end @@ -447,7 +465,7 @@ 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", Type::Date.new + m.register_type "date", OID::Date.new m.register_type "money", OID::Money.new m.register_type "bytea", OID::Bytea.new @@ -819,6 +837,7 @@ module ActiveRecord 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, OID::Date, adapter: :postgresql) ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgresql) ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql) ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index f34b6733da..c29cf1f9a1 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -28,7 +28,7 @@ module ActiveRecord coder["columns_hash"] = @columns_hash coder["primary_keys"] = @primary_keys coder["data_sources"] = @data_sources - coder["version"] = ActiveRecord::Migrator.current_version + coder["version"] = connection.migration_context.current_version end def init_with(coder) @@ -100,7 +100,7 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. - @version = ActiveRecord::Migrator.current_version + @version = connection.migration_context.current_version [@version, @columns, @columns_hash, @primary_keys, @data_sources] end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index 8042dbfea2..abedf01f10 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -17,7 +17,8 @@ module ActiveRecord end def quoted_time(value) - quoted_date(value) + value = value.change(year: 2000, month: 1, day: 1) + quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ") end def quoted_binary(value) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb index 58e5138e02..24e7bc65fa 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -7,6 +7,10 @@ module ActiveRecord # Returns an array of indexes for the given table. def indexes(table_name) exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| + # Indexes SQLite creates implicitly for internal use start with "sqlite_". + # See https://www.sqlite.org/fileformat2.html#intschema + next if row["name"].starts_with?("sqlite_") + index_sql = query_value(<<-SQL, "SCHEMA") SELECT sql FROM sqlite_master @@ -40,7 +44,7 @@ module ActiveRecord where: where, orders: orders ) - end + end.compact end def create_schema_dumper(options) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 670afa3684..c61e94f159 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -15,6 +15,8 @@ require "sqlite3" module ActiveRecord module ConnectionHandling # :nodoc: def sqlite3_connection(config) + config = config.symbolize_keys + # Require database. unless config[:database] raise ArgumentError, "No database file specified. Missing argument: database" @@ -31,7 +33,7 @@ module ActiveRecord db = SQLite3::Database.new( config[:database].to_s, - results_as_hash: true + config.merge(results_as_hash: true) ) db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] @@ -60,7 +62,7 @@ module ActiveRecord include SQLite3::SchemaStatements NATIVE_DATABASE_TYPES = { - primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL", + primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL", string: { name: "varchar" }, text: { name: "text" }, integer: { name: "integer" }, @@ -70,7 +72,8 @@ module ActiveRecord time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, - boolean: { name: "boolean" } + boolean: { name: "boolean" }, + json: { name: "json" }, } ## @@ -90,20 +93,23 @@ module ActiveRecord # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true class_attribute :represent_boolean_as_integer, default: false - class StatementPool < ConnectionAdapters::StatementPool + class StatementPool < ConnectionAdapters::StatementPool # :nodoc: private - def dealloc(stmt) - stmt[:stmt].close unless stmt[:stmt].closed? + stmt.close unless stmt.closed? end end def initialize(connection, logger, connection_options, config) super(connection, logger, config) - @active = nil + @active = true @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) + if sqlite_version < "3.8.0" + raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8." + end + configure_connection end @@ -116,7 +122,7 @@ module ActiveRecord end def supports_partial_index? - sqlite_version >= "3.8.0" + true end def requires_reloading? @@ -124,7 +130,7 @@ module ActiveRecord end def supports_foreign_keys_in_create? - sqlite_version >= "3.6.19" + true end def supports_views? @@ -135,12 +141,12 @@ module ActiveRecord true end - def supports_multi_insert? - sqlite_version >= "3.7.11" + def supports_json? + true end def active? - @active != false + @active end # Disconnects from the database if already connected. Otherwise, this @@ -183,13 +189,16 @@ module ActiveRecord # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity # :nodoc: - old = query_value("PRAGMA foreign_keys") + old_foreign_keys = query_value("PRAGMA foreign_keys") + old_defer_foreign_keys = query_value("PRAGMA defer_foreign_keys") begin + execute("PRAGMA defer_foreign_keys = ON") execute("PRAGMA foreign_keys = OFF") yield ensure - execute("PRAGMA foreign_keys = #{old}") + execute("PRAGMA defer_foreign_keys = #{old_defer_foreign_keys}") + execute("PRAGMA foreign_keys = #{old_foreign_keys}") end end @@ -220,11 +229,8 @@ module ActiveRecord stmt.close end else - cache = @statements[sql] ||= { - stmt: @connection.prepare(sql) - } - stmt = cache[:stmt] - cols = cache[:cols] ||= stmt.columns + stmt = @statements[sql] ||= @connection.prepare(sql) + cols = stmt.columns stmt.reset! stmt.bind_params(type_casted_binds) records = stmt.to_a @@ -286,19 +292,18 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end - # See: https://www.sqlite.org/lang_altertable.html - # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_type?(type) - type.to_sym != :primary_key + def valid_alter_table_type?(type, options = {}) + !invalid_alter_table_type?(type, options) end + deprecate :valid_alter_table_type? def add_column(table_name, column_name, type, options = {}) #:nodoc: - if valid_alter_table_type?(type) - super(table_name, column_name, type, options) - else + if invalid_alter_table_type?(type, options) alter_table(table_name) do |definition| definition.column(column_name, type, options) end + else + super end end @@ -364,12 +369,30 @@ module ActiveRecord end def insert_fixtures(rows, table_name) - rows.each do |row| - insert_fixture(row, table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `insert_fixtures` is deprecated and will be removed in the next version of Rails. + Consider using `insert_fixtures_set` for performance improvement. + MSG + insert_fixtures_set(table_name => rows) + end + + def insert_fixtures_set(fixture_set, tables_to_delete = []) + disable_referential_integrity do + transaction(requires_new: true) do + tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" } + + fixture_set.each do |table_name, rows| + rows.each { |row| insert_fixture(row, table_name) } + end + end end end private + def initialize_type_map(m = type_map) + super + register_class_with_limit m, %r(int)i, SQLite3Integer + end def table_structure(table_name) structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA") @@ -378,14 +401,33 @@ module ActiveRecord end alias column_definitions table_structure + # See: https://www.sqlite.org/lang_altertable.html + # SQLite has an additional restriction on the ALTER TABLE statement + def invalid_alter_table_type?(type, options) + type.to_sym == :primary_key || options[:primary_key] + end + def alter_table(table_name, options = {}) altered_table_name = "a#{table_name}" - caller = lambda { |definition| yield definition if block_given? } + foreign_keys = foreign_keys(table_name) + + caller = lambda do |definition| + rename = options[:rename] || {} + foreign_keys.each do |fk| + if column = rename[fk.options[:column]] + fk.options[:column] = column + end + definition.foreign_key(fk.to_table, fk.options) + end + + yield definition if block_given? + end transaction do - move_table(table_name, altered_table_name, - options.merge(temporary: true)) - move_table(altered_table_name, table_name, &caller) + disable_referential_integrity do + move_table(table_name, altered_table_name, options.merge(temporary: true)) + move_table(altered_table_name, table_name, &caller) + end end end @@ -399,19 +441,23 @@ module ActiveRecord options[:id] = false create_table(to, options) do |definition| @definition = definition - @definition.primary_key(from_primary_key) if from_primary_key.present? + if from_primary_key.is_a?(Array) + @definition.primary_keys from_primary_key + end columns(from).each do |column| column_name = options[:rename] ? (options[:rename][column.name] || options[:rename][column.name.to_sym] || column.name) : column.name - next if column_name == from_primary_key @definition.column(column_name, column.type, limit: column.limit, default: column.default, precision: column.precision, scale: column.scale, - null: column.null, collation: column.collation) + null: column.null, collation: column.collation, + primary_key: column_name == from_primary_key + ) end + yield @definition if block_given? end copy_table_indexes(from, to, options[:rename] || {}) @@ -438,6 +484,7 @@ module ActiveRecord # index name can't be the same opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true } opts[:unique] = true if index.unique + opts[:where] = index.where if index.where add_index(to, columns, opts) end end @@ -525,6 +572,17 @@ module ActiveRecord def configure_connection execute("PRAGMA foreign_keys = ON", "SCHEMA") end + + class SQLite3Integer < Type::Integer # :nodoc: + private + def _limit + # INTEGER storage class can be stored 8 bytes value. + # See https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes + limit || 8 + end + end + + ActiveRecord::Type.register(:integer, SQLite3Integer, adapter: :sqlite3) end ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter) end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 9a47edfba4..ee0e651912 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -57,6 +57,10 @@ module ActiveRecord spec = resolver.resolve(config).symbolize_keys spec[:name] = spec_name + # use the primary config if a config is not passed in and + # it's a three tier config + spec = spec[spec_name.to_sym] if spec[spec_name.to_sym] + connection_handler.establish_connection(spec) end @@ -140,6 +144,6 @@ module ActiveRecord end delegate :clear_active_connections!, :clear_reloadable_connections!, - :clear_all_connections!, to: :connection_handler + :clear_all_connections!, :flush_idle_connections!, to: :connection_handler end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 0f7a503c90..c983bc0d93 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -18,6 +18,13 @@ module ActiveRecord mattr_accessor :logger, instance_writer: false ## + # :singleton-method: + # + # Specifies if the methods calling database queries should be logged below + # their relevant queries. Defaults to false. + mattr_accessor :verbose_query_logs, instance_writer: false, default: false + + ## # Contains the database configuration - as is typically stored in config/database.yml - # as a Hash. # @@ -76,6 +83,14 @@ module ActiveRecord # scope being ignored is error-worthy, rather than a warning. mattr_accessor :error_on_ignored_order, instance_writer: false, default: false + # :singleton-method: + # Specify the behavior for unsafe raw query methods. Values are as follows + # deprecated - Warnings are logged when unsafe raw SQL is passed to + # query methods. + # disabled - Unsafe raw SQL passed to query methods results in + # UnknownAttributeReference exception. + mattr_accessor :allow_unsafe_raw_sql, instance_writer: false, default: :deprecated + ## # :singleton-method: # Specify whether or not to use timestamps for migration versions @@ -84,7 +99,7 @@ module ActiveRecord ## # :singleton-method: # Specify whether schema dump should happen at the end of the - # db:migrate rake task. This is true by default, which is useful for the + # db:migrate rails command. This is true by default, which is useful for the # development environment. This should ideally be false in the production # environment where dumping schema is rarely needed. mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true @@ -124,11 +139,6 @@ module ActiveRecord end module ClassMethods # :nodoc: - def allocate - define_attribute_methods - super - end - def initialize_find_by_cache # :nodoc: @find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new } end @@ -266,10 +276,11 @@ module ActiveRecord end def relation - relation = Relation.create(self, arel_table, predicate_builder) + relation = Relation.create(self) if finder_needs_type_condition? && !ignore_default_scope? - relation.where(type_condition).create_with(inheritance_column.to_s => sti_name) + relation.where!(type_condition) + relation.create_with!(inheritance_column.to_s => sti_name) else relation end @@ -334,6 +345,28 @@ module ActiveRecord end ## + # Initializer used for instantiating objects that have been read from the + # database. +attributes+ should be an attributes object, and unlike the + # `initialize` method, no assignment calls are made per attribute. + # + # :nodoc: + def init_from_db(attributes) + init_internals + + @new_record = false + @attributes = attributes + + self.class.define_attribute_methods + + yield self if block_given? + + _run_find_callbacks + _run_initialize_callbacks + + self + end + + ## # :method: clone # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied. # That means that modifying attributes of the clone will modify the original, since they will both point to the @@ -366,8 +399,10 @@ module ActiveRecord _run_initialize_callbacks - @new_record = true - @destroyed = false + @new_record = true + @destroyed = false + @_start_transaction_state = {} + @transaction_state = nil super end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index ee4f818cbf..c7f0077a76 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -47,8 +47,12 @@ module ActiveRecord 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 - updates = { counter_name.to_sym => object.send(counter_association).count(:all) } - updates.merge!(touch_updates(touch)) if touch + updates = { counter_name => object.send(counter_association).count(:all) } + + if touch + names = touch if touch != true + updates.merge!(touch_attributes_with_time(*names)) + end unscoped.where(primary_key => object.id).update_all(updates) end @@ -68,8 +72,8 @@ module ActiveRecord # * +counters+ - A Hash containing the names of the fields # to update as keys and the amount to update the field by as values. # * <tt>:touch</tt> option - Touch timestamp columns when updating. - # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to - # touch that column or an array of symbols to touch just those ones. + # If attribute names are passed, they are updated along with updated_at/on + # attributes. # # ==== Examples # @@ -98,20 +102,7 @@ module ActiveRecord # # `updated_at` = '2016-10-13T09:59:23-05:00' # # WHERE id IN (10, 15) def update_counters(id, counters) - touch = counters.delete(:touch) - - updates = counters.map do |counter_name, value| - operator = value < 0 ? "-" : "+" - quoted_column = connection.quote_column_name(counter_name) - "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" - end - - if touch - touch_updates = touch_updates(touch) - updates << sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty? - end - - unscoped.where(primary_key => id).update_all updates.join(", ") + unscoped.where!(primary_key => id).update_counters(counters) end # Increment a numeric field by one, via a direct SQL update. @@ -165,13 +156,6 @@ module ActiveRecord def decrement_counter(counter_name, id, touch: nil) update_counters(id, counter_name => -1, touch: touch) end - - private - def touch_updates(touch) - touch = timestamp_attributes_for_update_in_model if touch == true - touch_time = current_time_from_proper_timezone - Array(touch).map { |column| [ column, touch_time ] }.to_h - end end private diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb new file mode 100644 index 0000000000..ffeed45030 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module ActiveRecord + module DatabaseConfigurations # :nodoc: + class DatabaseConfig + attr_reader :env_name, :spec_name, :config + + def initialize(env_name, spec_name, config) + @env_name = env_name + @spec_name = spec_name + @config = config + end + end + + # Selects the config for the specified environment and specification name + # + # For example if passed :development, and :animals it will select the database + # under the :development and :animals configuration level + def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc: + configs_for(environment, configs).find do |db_config| + db_config.spec_name == specification_name + end + end + + # Collects the configs for the environment passed in. + # + # If a block is given returns the specification name and configuration + # otherwise returns an array of DatabaseConfig structs for the environment. + def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc: + env_with_configs = db_configs(configs).select do |db_config| + db_config.env_name == env + end + + if block_given? + env_with_configs.each do |env_with_config| + yield env_with_config.spec_name, env_with_config.config + end + else + env_with_configs + end + end + + # Given an env, spec and config creates DatabaseConfig structs with + # each attribute set. + def self.walk_configs(env_name, spec_name, config) # :nodoc: + if config["database"] || config["url"] || config["adapter"] + DatabaseConfig.new(env_name, spec_name, config) + else + config.each_pair.map do |sub_spec_name, sub_config| + walk_configs(env_name, sub_spec_name, sub_config) + end + end + end + + # Walks all the configs passed in and returns an array + # of DatabaseConfig structs for each configuration. + def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc: + configs.each_pair.flat_map do |env_name, config| + walk_configs(env_name, "primary", config) + end + end + end +end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index f373b98035..23ecb24542 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -141,10 +141,7 @@ module ActiveRecord end end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :name, :mapping, :subtype end @@ -221,6 +218,8 @@ module ActiveRecord def detect_enum_conflict!(enum_name, method_name, klass_method = false) if klass_method && dangerous_class_method?(method_name) raise_conflict_error(enum_name, method_name, type: "class") + elsif klass_method && method_defined_within?(method_name, Relation) + raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name) elsif !klass_method && dangerous_attribute_method?(method_name) raise_conflict_error(enum_name, method_name) elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module) diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 9ef3316393..f61bc7b9e8 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -111,7 +111,8 @@ module ActiveRecord class RecordNotUnique < WrappedDatabaseException end - # Raised when a record cannot be inserted or updated because it references a non-existent record. + # Raised when a record cannot be inserted or updated because it references a non-existent record, + # or when a record cannot be deleted because a parent record references it. class InvalidForeignKey < WrappedDatabaseException end @@ -120,13 +121,13 @@ module ActiveRecord def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil) @adapter = adapter if table - msg = <<-EOM.strip_heredoc + msg = +<<~EOM Column `#{foreign_key}` on table `#{table}` has a type of `#{column_type(table, foreign_key)}`. This does not match column `#{primary_key}` on `#{target_table}`, which has type `#{column_type(target_table, primary_key)}`. To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :integer. (For example `t.integer #{foreign_key}`). EOM else - msg = <<-EOM + msg = +<<~EOM There is a mismatch between the foreign key and primary key column types. Verify that the foreign key column type and the primary key of the associated table match types. EOM @@ -335,8 +336,40 @@ module ActiveRecord class IrreversibleOrderError < ActiveRecordError end - # TransactionTimeout will be raised when lock wait timeout expires. - # Wait time value is set by innodb_lock_wait_timeout. - class TransactionTimeout < StatementInvalid + # LockWaitTimeout will be raised when lock wait timeout exceeded. + class LockWaitTimeout < StatementInvalid + end + + # StatementTimeout will be raised when statement timeout exceeded. + class StatementTimeout < StatementInvalid + end + + # QueryCanceled will be raised when canceling statement due to user request. + class QueryCanceled < StatementInvalid + end + + # UnknownAttributeReference is raised when an unknown and potentially unsafe + # value is passed to a query method when allow_unsafe_raw_sql is set to + # :disabled. For example, passing a non column name value to a relation's + # #order method might cause this exception. + # + # When working around this exception, caution should be taken to avoid SQL + # injection vulnerabilities when passing user-provided values to query + # methods. Known-safe values can be passed to query methods by wrapping them + # in Arel.sql. + # + # For example, with allow_unsafe_raw_sql set to :disabled, the following + # code would raise this exception: + # + # Post.order("length(title)").first + # + # The desired result can be accomplished by wrapping the known-safe string + # in Arel.sql: + # + # Post.order(Arel.sql("length(title)")).first + # + # Again, such a workaround should *not* be used when passing user-provided + # values, such as request parameters or model attributes to query methods. + class UnknownAttributeReference < ActiveRecordError end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 86f13d75d5..6e0f1a0dfb 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -169,18 +169,18 @@ module ActiveRecord # self.use_transactional_tests = true # # test "godzilla" do - # assert !Foo.all.empty? + # assert_not_empty Foo.all # Foo.destroy_all - # assert Foo.all.empty? + # assert_empty Foo.all # end # # test "godzilla aftermath" do - # assert !Foo.all.empty? + # assert_not_empty Foo.all # end # end # - # If you preload your test database with all fixture data (probably in the rake task) and use - # transactional tests, then you may omit all fixtures declarations in your test cases since + # If you preload your test database with all fixture data (probably by running `rails db:fixtures:load`) + # and use transactional tests, then you may omit all fixtures declarations in your test cases since # all the data's already there and every case rolls back its changes. # # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to @@ -540,47 +540,38 @@ module ActiveRecord } unless files_to_read.empty? - connection.disable_referential_integrity do - fixtures_map = {} - - fixture_sets = files_to_read.map do |fs_name| - klass = class_names[fs_name] - conn = klass ? klass.connection : connection - fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new - conn, - fs_name, - klass, - ::File.join(fixtures_directory, fs_name)) - end - - update_all_loaded_fixtures fixtures_map - - connection.transaction(requires_new: true) do - deleted_tables = Hash.new { |h, k| h[k] = Set.new } - fixture_sets.each do |fs| - conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection - table_rows = fs.table_rows + fixtures_map = {} + + fixture_sets = files_to_read.map do |fs_name| + klass = class_names[fs_name] + conn = klass ? klass.connection : connection + fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new + conn, + fs_name, + klass, + ::File.join(fixtures_directory, fs_name)) + end - table_rows.each_key do |table| - unless deleted_tables[conn].include? table - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete" - end - deleted_tables[conn] << table - end + update_all_loaded_fixtures fixtures_map + fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection } - table_rows.each do |fixture_set_name, rows| - conn.insert_fixtures(rows, fixture_set_name) - end + fixture_sets_by_connection.each do |conn, set| + table_rows_for_connection = Hash.new { |h, k| h[k] = [] } - # Cap primary key sequences to max(pk). - if conn.respond_to?(:reset_pk_sequence!) - conn.reset_pk_sequence!(fs.table_name) - end + set.each do |fs| + fs.table_rows.each do |table, rows| + table_rows_for_connection[table].unshift(*rows) end end + conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys) - cache_fixtures(connection, fixtures_map) + # Cap primary key sequences to max(pk). + if conn.respond_to?(:reset_pk_sequence!) + set.each { |fs| conn.reset_pk_sequence!(fs.table_name) } + end end + + cache_fixtures(connection, fixtures_map) end cached_fixtures(connection, fixture_set_names) end @@ -883,6 +874,7 @@ module ActiveRecord class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances class_attribute :pre_loaded_fixtures, default: false class_attribute :config, default: ActiveRecord::Base + class_attribute :lock_threads, default: true end module ClassMethods @@ -982,7 +974,7 @@ module ActiveRecord @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| connection.begin_transaction joinable: false - connection.pool.lock_thread = true + connection.pool.lock_thread = true if lock_threads end # When connections are established in the future, begin a transaction too @@ -998,7 +990,7 @@ module ActiveRecord if connection && !@fixture_connections.include?(connection) connection.begin_transaction joinable: false - connection.pool.lock_thread = true + connection.pool.lock_thread = true if lock_threads @fixture_connections << connection end end diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 7ccb57b305..72035a986b 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -7,8 +7,8 @@ module ActiveRecord end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 0715c11473..6891c575c7 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -47,22 +47,21 @@ module ActiveRecord # Determines if one of the attributes passed in is the inheritance column, # and if the inheritance column is attr accessible, it initializes an # instance of the given subclass instead of the base class. - def new(*args, &block) + def new(attributes = nil, &block) if abstract_class? || self == Base raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated." end - attrs = args.first if has_attribute?(inheritance_column) - subclass = subclass_from_attributes(attrs) + subclass = subclass_from_attributes(attributes) - if subclass.nil? && base_class == self + if subclass.nil? && base_class? subclass = subclass_from_attributes(column_defaults) end end if subclass && subclass != self - subclass.new(*args, &block) + subclass.new(attributes, &block) else super end @@ -105,21 +104,53 @@ module ActiveRecord end end - # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). - # If you are using inheritance with ActiveRecord and don't want child classes - # to utilize the implied STI table name of the parent class, this will need to be true. - # For example, given the following: + # Returns whether the class is a base class. + # See #base_class for more information. + def base_class? + base_class == self + end + + # Set this to +true+ if this is an abstract class (see + # <tt>abstract_class?</tt>). + # If you are using inheritance with Active Record and don't want a class + # to be considered as part of the STI hierarchy, you must set this to + # true. + # +ApplicationRecord+, for example, is generated as an abstract class. + # + # Consider the following default behaviour: + # + # Shape = Class.new(ActiveRecord::Base) + # Polygon = Class.new(Shape) + # Square = Class.new(Polygon) + # + # Shape.table_name # => "shapes" + # Polygon.table_name # => "shapes" + # Square.table_name # => "shapes" + # Shape.create! # => #<Shape id: 1, type: nil> + # Polygon.create! # => #<Polygon id: 2, type: "Polygon"> + # Square.create! # => #<Square id: 3, type: "Square"> # - # class SuperClass < ActiveRecord::Base + # However, when using <tt>abstract_class</tt>, +Shape+ is omitted from + # the hierarchy: + # + # class Shape < ActiveRecord::Base # self.abstract_class = true # end - # class Child < SuperClass - # self.table_name = 'the_table_i_really_want' - # end - # + # Polygon = Class.new(Shape) + # Square = Class.new(Polygon) # - # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt> + # Shape.table_name # => nil + # Polygon.table_name # => "polygons" + # Square.table_name # => "polygons" + # Shape.create! # => NotImplementedError: Shape is an abstract class and cannot be instantiated. + # Polygon.create! # => #<Polygon id: 1, type: nil> + # Square.create! # => #<Square id: 2, type: "Square"> # + # Note that in the above example, to disallow the creation of a plain + # +Polygon+, you should use <tt>validates :type, presence: true</tt>, + # instead of setting it as an abstract class. This way, +Polygon+ will + # stay in the hierarchy, and Active Record will continue to correctly + # derive the table name. attr_accessor :abstract_class # Returns whether this class is an abstract class or not. @@ -131,6 +162,10 @@ module ActiveRecord store_full_sti_class ? name : name.demodulize end + def polymorphic_name + base_class.name + end + def inherited(subclass) subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new) super diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 5a65edf27e..3626a13d7c 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -17,7 +17,7 @@ module ActiveRecord end def []=(key, value) - find_or_initialize_by(key: key).update_attributes!(value: value) + find_or_initialize_by(key: key).update!(value: value) end def [](key) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index e1e24e2814..1030b2368b 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -61,13 +61,6 @@ module ActiveRecord end private - - def increment_lock - lock_col = self.class.locking_column - previous_lock_value = send(lock_col) - send("#{lock_col}=", previous_lock_value + 1) - end - def _create_record(attribute_names = self.attribute_names, *) if locking_enabled? # We always want to persist the locking version, even if we don't detect @@ -77,63 +70,56 @@ module ActiveRecord super end - def _update_record(attribute_names = self.attribute_names) + def _touch_row(attribute_names, time) + super + ensure + clear_attribute_change(self.class.locking_column) if locking_enabled? + end + + def _update_row(attribute_names, attempted_action = "update") return super unless locking_enabled? - return 0 if attribute_names.empty? begin - lock_col = self.class.locking_column - - previous_lock_value = read_attribute_before_type_cast(lock_col) - - increment_lock - - attribute_names.push(lock_col) + locking_column = self.class.locking_column + previous_lock_value = read_attribute_before_type_cast(locking_column) + attribute_names << locking_column - relation = self.class.unscoped + self[locking_column] += 1 - affected_rows = relation.where( - self.class.primary_key => id, - lock_col => previous_lock_value - ).update_all( - attributes_for_update(attribute_names).map do |name| - [name, _read_attribute(name)] - end.to_h + affected_rows = self.class._update_record( + attributes_with_values(attribute_names), + self.class.primary_key => id_in_database, + locking_column => previous_lock_value ) - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "update") + if affected_rows != 1 + raise ActiveRecord::StaleObjectError.new(self, attempted_action) end affected_rows # If something went wrong, revert the locking_column value. rescue Exception - send("#{lock_col}=", previous_lock_value.to_i) - + self[locking_column] = previous_lock_value.to_i raise end end def destroy_row - affected_rows = super - - if locking_enabled? && affected_rows != 1 - raise ActiveRecord::StaleObjectError.new(self, "destroy") - end + return super unless locking_enabled? - affected_rows - end + locking_column = self.class.locking_column - def relation_for_destroy - relation = super + affected_rows = self.class._delete_record( + self.class.primary_key => id_in_database, + locking_column => read_attribute_before_type_cast(locking_column) + ) - if locking_enabled? - locking_column = self.class.locking_column - relation = relation.where(locking_column => read_attribute_before_type_cast(locking_column)) + if affected_rows != 1 + raise ActiveRecord::StaleObjectError.new(self, "destroy") end - relation + affected_rows end module ClassMethods @@ -179,7 +165,7 @@ module ActiveRecord 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| + decorate_matching_attribute_types(is_lock_column, "_optimistic_locking") do |type| LockingType.new(type) end end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index bb85c47e06..5d1d15c94d 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -62,7 +62,7 @@ module ActiveRecord # the locked record. def lock!(lock = true) if persisted? - if changed? + if has_changes_to_save? raise(<<-MSG.squish) Locking a record with unpersisted changes is not supported. Use `save` to persist the changes, or `reload` to discard them diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 405f3a30c6..6b84431343 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -4,6 +4,8 @@ module ActiveRecord class LogSubscriber < ActiveSupport::LogSubscriber IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"] + class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new + def self.runtime=(value) ActiveRecord::RuntimeRegistry.sql_runtime = value end @@ -90,6 +92,26 @@ module ActiveRecord def logger ActiveRecord::Base.logger end + + def debug(progname = nil, &block) + return unless super + + if ActiveRecord::Base.verbose_query_logs + log_query_source + end + end + + def log_query_source + source = extract_query_source_location(caller) + + if source + logger.debug(" ↳ #{source}") + end + end + + def extract_query_source_location(locations) + backtrace_cleaner.clean(locations).first + end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index d12a979a7f..d68ed3cad9 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "benchmark" require "set" require "zlib" require "active_support/core_ext/module/attribute_accessors" @@ -129,9 +130,9 @@ module ActiveRecord class PendingMigrationError < MigrationError#:nodoc: def initialize(message = nil) if !message && defined?(Rails.env) - super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate RAILS_ENV=#{::Rails.env}") + super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate RAILS_ENV=#{::Rails.env}") elsif !message - super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate") + super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate") else super end @@ -140,6 +141,7 @@ module ActiveRecord class ConcurrentMigrationError < MigrationError #:nodoc: DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze + RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock".freeze def initialize(message = DEFAULT_MESSAGE) super @@ -148,7 +150,7 @@ module ActiveRecord class NoEnvironmentInSchemaError < MigrationError #:nodoc: def initialize - msg = "Environment data not found in the schema. To resolve this issue, run: \n\n bin/rails db:environment:set" + msg = "Environment data not found in the schema. To resolve this issue, run: \n\n rails db:environment:set" if defined?(Rails.env) super("#{msg} RAILS_ENV=#{::Rails.env}") else @@ -171,7 +173,7 @@ module ActiveRecord msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n".dup msg << "You are running in `#{ current }` environment. " msg << "If you are sure you want to continue, first set the environment using:\n\n" - msg << " bin/rails db:environment:set" + msg << " rails db:environment:set" if defined?(Rails.env) super("#{msg} RAILS_ENV=#{::Rails.env}\n\n") else @@ -350,7 +352,7 @@ module ActiveRecord # <tt>rails db:migrate</tt>. This will update the database by running all of the # pending migrations, creating the <tt>schema_migrations</tt> table # (see "About the schema_migrations table" section below) if missing. It will also - # invoke the db:schema:dump task, which will update your db/schema.rb file + # invoke the db:schema:dump command, which will update your db/schema.rb file # to match the structure of your database. # # To roll the database back to a previous migration version, use @@ -550,7 +552,7 @@ module ActiveRecord end def call(env) - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + mtime = ActiveRecord::Base.connection.migration_context.last_migration.mtime.to_i if @last_check < mtime ActiveRecord::Migration.check_pending!(connection) @last_check = mtime @@ -575,13 +577,14 @@ module ActiveRecord # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending. def check_pending!(connection = Base.connection) - raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection) + raise ActiveRecord::PendingMigrationError if connection.migration_context.needs_migration? end def load_schema_if_pending! - if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? + if Base.connection.migration_context.needs_migration? || !Base.connection.migration_context.any_migrations? # Roundtrip to Rake to allow plugins to hook into database initialization. - FileUtils.cd Rails.root do + root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root + FileUtils.cd(root) do current_config = Base.connection_config Base.clear_all_connections! system("bin/rails db:test:prepare") @@ -733,6 +736,24 @@ module ActiveRecord execute_block { yield helper } end + # Used to specify an operation that is only run when migrating up + # (for example, populating a new column with its initial values). + # + # In the following example, the new column +published+ will be given + # the value +true+ for all existing records. + # + # class AddPublishedToPosts < ActiveRecord::Migration[5.2] + # def change + # add_column :posts, :published, :boolean, default: false + # up_only do + # execute "update posts set published = 'true'" + # end + # end + # end + def up_only + execute_block { yield } unless reverting? + end + # Runs the given migration classes. # Last argument can specify options: # - :direction (default is :up) @@ -810,10 +831,14 @@ module ActiveRecord write "== %s %s" % [text, "=" * length] end + # Takes a message argument and outputs it as is. + # A second boolean argument can be passed to specify whether to indent or not. def say(message, subitem = false) write "#{subitem ? " ->" : "--"} #{message}" end + # Outputs text along with how long it took to run its block. + # If the block returns an integer it assumes it is the number of rows affected. def say_with_time(message) say(message) result = nil @@ -823,6 +848,7 @@ module ActiveRecord result end + # Takes a block as an argument and suppresses any output generated by the block. def suppress_messages save, self.verbose = verbose, false yield @@ -857,10 +883,10 @@ module ActiveRecord FileUtils.mkdir_p(destination) unless File.exist?(destination) - destination_migrations = ActiveRecord::Migrator.migrations(destination) + destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations last = destination_migrations.last sources.each do |scope, path| - source_migrations = ActiveRecord::Migrator.migrations(path) + source_migrations = ActiveRecord::MigrationContext.new(path).migrations source_migrations.each do |migration| source = File.binread(migration.filename) @@ -978,131 +1004,147 @@ module ActiveRecord end end - class Migrator#:nodoc: - class << self - attr_writer :migrations_paths - alias :migrations_path= :migrations_paths= - - def migrate(migrations_paths, target_version = nil, &block) - case - when target_version.nil? - up(migrations_paths, target_version, &block) - when current_version == 0 && target_version == 0 - [] - when current_version > target_version - down(migrations_paths, target_version, &block) - else - up(migrations_paths, target_version, &block) - end - end + class MigrationContext # :nodoc: + attr_reader :migrations_paths - def rollback(migrations_paths, steps = 1) - move(:down, migrations_paths, steps) - end + def initialize(migrations_paths) + @migrations_paths = migrations_paths + end - def forward(migrations_paths, steps = 1) - move(:up, migrations_paths, steps) + def migrate(target_version = nil, &block) + case + when target_version.nil? + up(target_version, &block) + when current_version == 0 && target_version == 0 + [] + when current_version > target_version + down(target_version, &block) + else + up(target_version, &block) end + end + + def rollback(steps = 1) + move(:down, steps) + end - def up(migrations_paths, target_version = nil) - migrations = migrations(migrations_paths) - migrations.select! { |m| yield m } if block_given? + def forward(steps = 1) + move(:up, steps) + end - new(:up, migrations, target_version).migrate + def up(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations end - def down(migrations_paths, target_version = nil) - migrations = migrations(migrations_paths) - migrations.select! { |m| yield m } if block_given? + Migrator.new(:up, selected_migrations, target_version).migrate + end - new(:down, migrations, target_version).migrate + def down(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations end - def run(direction, migrations_paths, target_version) - new(direction, migrations(migrations_paths), target_version).run - end + Migrator.new(:down, selected_migrations, target_version).migrate + end - def open(migrations_paths) - new(:up, migrations(migrations_paths), nil) - end + def run(direction, target_version) + Migrator.new(direction, migrations, target_version).run + end - def get_all_versions(connection = Base.connection) - if SchemaMigration.table_exists? - SchemaMigration.all_versions.map(&:to_i) - else - [] - end - end + def open + Migrator.new(:up, migrations, nil) + end - def current_version(connection = Base.connection) - get_all_versions(connection).max || 0 + def get_all_versions + if SchemaMigration.table_exists? + SchemaMigration.all_versions.map(&:to_i) + else + [] end + end - def needs_migration?(connection = Base.connection) - (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0 - end + def current_version + get_all_versions.max || 0 + rescue ActiveRecord::NoDatabaseError + end - def any_migrations? - migrations(migrations_paths).any? - end + def needs_migration? + (migrations.collect(&:version) - get_all_versions).size > 0 + end - def last_migration #:nodoc: - migrations(migrations_paths).last || NullMigration.new - end + def any_migrations? + migrations.any? + end - def migrations_paths - @migrations_paths ||= ["db/migrate"] - # just to not break things if someone uses: migrations_path = some_string - Array(@migrations_paths) - end + def last_migration #:nodoc: + migrations.last || NullMigration.new + end + + def parse_migration_filename(filename) # :nodoc: + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + end + + def migrations + migrations = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = version.to_i + name = name.camelize - def parse_migration_filename(filename) # :nodoc: - File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + MigrationProxy.new(name, version, file, scope) end - def migrations(paths) - paths = Array(paths) + migrations.sort_by(&:version) + end - migrations = migration_files(paths).map do |file| - version, name, scope = parse_migration_filename(file) - raise IllegalMigrationNameError.new(file) unless version - version = version.to_i - name = name.camelize + def migrations_status + db_list = ActiveRecord::SchemaMigration.normalized_versions - MigrationProxy.new(name, version, file, scope) - end + file_list = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = ActiveRecord::SchemaMigration.normalize_migration_number(version) + status = db_list.delete(version) ? "up" : "down" + [status, version, (name + scope).humanize] + end.compact - migrations.sort_by(&:version) + db_list.map! do |version| + ["up", version, "********** NO FILE **********"] end - def migrations_status(paths) - paths = Array(paths) - - db_list = ActiveRecord::SchemaMigration.normalized_versions + (db_list + file_list).sort_by { |_, version, _| version } + end - file_list = migration_files(paths).map do |file| - version, name, scope = parse_migration_filename(file) - raise IllegalMigrationNameError.new(file) unless version - version = ActiveRecord::SchemaMigration.normalize_migration_number(version) - status = db_list.delete(version) ? "up" : "down" - [status, version, (name + scope).humanize] - end.compact + def migration_files + paths = Array(migrations_paths) + Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] + end - db_list.map! do |version| - ["up", version, "********** NO FILE **********"] - end + def current_environment + ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end - (db_list + file_list).sort_by { |_, version, _| version } - end + def protected_environment? + ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment + end - def migration_files(paths) - Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] - end + def last_stored_environment + return nil if current_version == 0 + raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? - private + environment = ActiveRecord::InternalMetadata[:environment] + raise NoEnvironmentInSchemaError unless environment + environment + end - def move(direction, migrations_paths, steps) - migrator = new(direction, migrations(migrations_paths)) + private + def move(direction, steps) + migrator = Migrator.new(direction, migrations) if current_version != 0 && !migrator.current_migration raise UnknownMigrationVersionError.new(current_version) @@ -1117,10 +1159,29 @@ module ActiveRecord finish = migrator.migrations[start_index + steps] version = finish ? finish.version : 0 - send(direction, migrations_paths, version) + send(direction, version) + end + end + + class Migrator # :nodoc: + class << self + attr_accessor :migrations_paths + + def migrations_path=(path) + ActiveSupport::Deprecation.warn \ + "ActiveRecord::Migrator.migrations_paths= is now deprecated and will be removed in Rails 6.0." \ + "You can set the `migrations_paths` on the `connection` instead through the `database.yml`." + self.migrations_paths = [path] + end + + # For cases where a table doesn't exist like loading from schema cache + def current_version + MigrationContext.new(migrations_paths).current_version end end + self.migrations_paths = ["db/migrate"] + def initialize(direction, migrations, target_version = nil) @direction = direction @target_version = target_version @@ -1183,7 +1244,7 @@ module ActiveRecord end def load_migrated - @migrated_versions = Set.new(self.class.get_all_versions) + @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions) end private @@ -1215,7 +1276,7 @@ module ActiveRecord # Stores the current environment in the database. def record_environment return if down? - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment end def ran?(migration) @@ -1257,10 +1318,10 @@ module ActiveRecord end def validate(migrations) - name , = migrations.group_by(&:name).find { |_, v| v.length > 1 } + name, = migrations.group_by(&:name).find { |_, v| v.length > 1 } raise DuplicateMigrationNameError.new(name) if name - version , = migrations.group_by(&:version).find { |_, v| v.length > 1 } + version, = migrations.group_by(&:version).find { |_, v| v.length > 1 } raise DuplicateMigrationVersionError.new(version) if version end @@ -1274,23 +1335,6 @@ module ActiveRecord end end - def self.last_stored_environment - return nil if current_version == 0 - raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? - - environment = ActiveRecord::InternalMetadata[:environment] - raise NoEnvironmentInSchemaError unless environment - environment - end - - def self.current_environment - ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - end - - def self.protected_environment? - ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment - end - def up? @direction == :up end @@ -1318,12 +1362,17 @@ module ActiveRecord def with_advisory_lock lock_id = generate_migrator_advisory_lock_id - got_lock = Base.connection.get_advisory_lock(lock_id) + connection = Base.connection + got_lock = connection.get_advisory_lock(lock_id) raise ConcurrentMigrationError unless got_lock load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock yield ensure - Base.connection.release_advisory_lock(lock_id) if got_lock + if got_lock && !connection.release_advisory_lock(lock_id) + raise ConcurrentMigrationError.new( + ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE + ) + end end MIGRATOR_SALT = 2053462845 diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index ac7d506fd1..dea6d4ec08 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -85,7 +85,7 @@ module ActiveRecord # invert the +command+. def inverse_of(command, args, &block) method = :"invert_#{command}" - raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true) + raise IrreversibleMigration, <<~MSG unless respond_to?(method, true) This migration uses #{command}, which is not automatically reversible. To make the migration reversible you can either: 1. Define #up and #down methods in place of the #change method. @@ -110,7 +110,7 @@ module ActiveRecord private - module StraightReversions + module StraightReversions # :nodoc: private { transaction: :transaction, execute_block: :execute_block, @@ -214,11 +214,24 @@ module ActiveRecord end def invert_remove_foreign_key(args) - from_table, to_table, remove_options = args - raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash) + from_table, options_or_to_table, options_or_nil = args + + to_table = if options_or_to_table.is_a?(Hash) + options_or_to_table[:to_table] + else + options_or_to_table + end + + remove_options = if options_or_to_table.is_a?(Hash) + options_or_to_table.except(:to_table) + else + options_or_nil + end + + raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? reversed_args = [from_table, to_table] - reversed_args << remove_options if remove_options + reversed_args << remove_options if remove_options.present? [:add_foreign_key, reversed_args] end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index c979aaf0a0..0edaaa0cf9 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -13,9 +13,32 @@ module ActiveRecord const_get(name) end - V5_2 = Current + V6_0 = Current + + class V5_2 < V6_0 + end class V5_1 < V5_2 + def change_column(table_name, column_name, type, options = {}) + if adapter_name == "PostgreSQL" + clear_cache! + sql = connection.send(:change_column_sql, table_name, column_name, type, options) + execute "ALTER TABLE #{quote_table_name(table_name)} #{sql}" + change_column_default(table_name, column_name, options[:default]) if options.key?(:default) + change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) + else + super + end + end + + def create_table(table_name, options = {}) + if adapter_name == "Mysql2" + super(table_name, options: "ENGINE=InnoDB", **options) + else + super + end + end end class V5_0 < V5_1 diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 12ee4a4137..694ff85fa1 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -87,19 +87,6 @@ module ActiveRecord # Sets the name of the internal metadata table. ## - # :singleton-method: protected_environments - # :call-seq: protected_environments - # - # The array of names of environments where destructive actions should be prohibited. By default, - # the value is <tt>["production"]</tt>. - - ## - # :singleton-method: protected_environments= - # :call-seq: protected_environments=(environments) - # - # Sets an array of names of environments where destructive actions should be prohibited. - - ## # :singleton-method: pluralize_table_names # :call-seq: pluralize_table_names # @@ -122,9 +109,9 @@ module ActiveRecord class_attribute :table_name_suffix, instance_writer: false, default: "" class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations" class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata" - class_attribute :protected_environments, instance_accessor: false, default: [ "production" ] class_attribute :pluralize_table_names, instance_writer: false, default: true + self.protected_environments = ["production"] self.inheritance_column = "type" self.ignored_columns = [].freeze @@ -238,6 +225,21 @@ module ActiveRecord (parents.detect { |p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix end + # The array of names of environments where destructive actions should be prohibited. By default, + # the value is <tt>["production"]</tt>. + def protected_environments + if defined?(@protected_environments) + @protected_environments + else + superclass.protected_environments + end + end + + # Sets an array of names of environments where destructive actions should be prohibited. + def protected_environments=(environments) + @protected_environments = environments.map(&:to_s) + end + # Defines the name of the table column which will store the class name on single-table # inheritance situations. # @@ -274,7 +276,7 @@ module ActiveRecord end def sequence_name - if base_class == self + if base_class? @sequence_name ||= reset_sequence_name else (@sequence_name ||= nil) || base_class.sequence_name @@ -323,11 +325,11 @@ module ActiveRecord end def attributes_builder # :nodoc: - @attributes_builder ||= ActiveModel::AttributeSet::Builder.new(attribute_types, primary_key) do |name| - unless columns_hash.key?(name) - _default_attributes[name].dup - end + unless defined?(@attributes_builder) && @attributes_builder + defaults = _default_attributes.except(*(column_names - [primary_key])) + @attributes_builder = ActiveModel::AttributeSet::Builder.new(attribute_types, defaults) end + @attributes_builder end def columns_hash # :nodoc: @@ -359,8 +361,9 @@ module ActiveRecord # it). # # +attr_name+ The name of the attribute to retrieve the type for. Must be - # a string + # a string or a symbol. def type_for_attribute(attr_name, &block) + attr_name = attr_name.to_s if block attribute_types.fetch(attr_name, &block) else @@ -376,6 +379,7 @@ module ActiveRecord end def _default_attributes # :nodoc: + load_schema @default_attributes ||= ActiveModel::AttributeSet.new({}) end @@ -423,7 +427,7 @@ module ActiveRecord # end def reset_column_information connection.clear_cache! - undefine_attribute_methods + ([self] + descendants).each(&:undefine_attribute_methods) connection.schema_cache.clear_data_source_cache!(table_name) reload_schema_from_cache @@ -497,8 +501,7 @@ module ActiveRecord # Computes and returns a table name according to default conventions. def compute_table_name - base = base_class - if self == base + if base_class? # Nested classes are prefixed with singular parent table name. if parent < Base && !parent.abstract_class? contained = parent.table_name @@ -509,7 +512,7 @@ module ActiveRecord "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}" else # STI subclasses always use their superclass' table. - base.table_name + base_class.table_name end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 435c81c153..50767ee93f 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -63,6 +63,18 @@ module ActiveRecord # member.update params[:member] # member.avatar.icon # => 'sad' # + # If you want to update the current avatar without providing the id, you must add <tt>:update_only</tt> option. + # + # class Member < ActiveRecord::Base + # has_one :avatar + # accepts_nested_attributes_for :avatar, update_only: true + # end + # + # params = { member: { avatar_attributes: { icon: 'sad' } } } + # member.update params[:member] + # member.avatar.id # => 2 + # member.avatar.icon # => 'sad' + # # By default you will only be able to set and update attributes on the # associated model. If you want to destroy the associated model through the # attributes hash, you have to enable it first using the @@ -489,7 +501,7 @@ module ActiveRecord if attributes["id"].blank? unless reject_new_record?(association_name, attributes) - association.build(attributes.except(*UNASSIGNABLE_KEYS)) + association.reader.build(attributes.except(*UNASSIGNABLE_KEYS)) end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes["id"].to_s } unless call_reject_if(association_name, attributes) diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb index 754c891884..697076bdae 100644 --- a/activerecord/lib/active_record/no_touching.rb +++ b/activerecord/lib/active_record/no_touching.rb @@ -43,6 +43,13 @@ module ActiveRecord end end + # Returns +true+ if the class has +no_touching+ set, +false+ otherwise. + # + # Project.no_touching do + # Project.first.no_touching? # true + # Message.first.no_touching? # false + # end + # def no_touching? NoTouching.applied_to?(self.class) end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a57c60ffac..05963e5546 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -67,8 +67,18 @@ module ActiveRecord # how this "single-table" inheritance mapping is implemented. def instantiate(attributes, column_types = {}, &block) klass = discriminate_class_for_record(attributes) + instantiate_instance_of(klass, attributes, column_types, &block) + end + + # Given a class, an attributes hash, +instantiate_instance_of+ returns a + # new instance of the class. Accepts only keys as strings. + # + # This is private, don't call it. :) + # + # :nodoc: + def instantiate_instance_of(klass, attributes, column_types = {}, &block) attributes = klass.attributes_builder.build_from_database(attributes, column_types) - klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block) + klass.allocate.init_from_db(attributes, &block) end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -97,11 +107,11 @@ module ActiveRecord # When running callbacks is not needed for each record update, # it is preferred to use {update_all}[rdoc-ref:Relation#update_all] # for updating all records in a single query. - def update(id = :all, attributes) + def update(id, attributes) if id.is_a?(Array) - id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }.compact - elsif id == :all - all.each { |record| record.update(attributes) } + id.map { |one_id| find(one_id) }.each_with_index { |object, idx| + object.update(attributes[idx]) + } else if ActiveRecord::Base === id raise ArgumentError, @@ -112,7 +122,6 @@ module ActiveRecord object.update(attributes) object end - rescue RecordNotFound end # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, @@ -136,11 +145,10 @@ module ActiveRecord # Todo.destroy(todos) def destroy(id) if id.is_a?(Array) - id.map { |one_id| destroy(one_id) }.compact + find(id).each(&:destroy) else find(id).destroy end - rescue RecordNotFound end # Deletes the row with a primary key matching the +id+ argument, using a @@ -165,6 +173,48 @@ module ActiveRecord where(primary_key => id_or_array).delete_all end + def _insert_record(values) # :nodoc: + primary_key_value = nil + + if primary_key && Hash === values + primary_key_value = values[primary_key] + + if !primary_key_value && prefetch_primary_key? + primary_key_value = next_sequence_value + values[primary_key] = primary_key_value + end + end + + if values.empty? + im = arel_table.compile_insert(connection.empty_insert_statement_value(primary_key)) + im.into arel_table + else + im = arel_table.compile_insert(_substitute_values(values)) + end + + connection.insert(im, "#{self} Create", primary_key || false, primary_key_value) + end + + def _update_record(values, constraints) # :nodoc: + constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) } + + um = arel_table.where( + constraints.reduce(&:and) + ).compile_update(_substitute_values(values), primary_key) + + connection.update(um, "#{self} Update") + end + + def _delete_record(constraints) # :nodoc: + constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) } + + dm = Arel::DeleteManager.new + dm.from(arel_table) + dm.wheres = constraints + + connection.delete(dm, "#{self} Destroy") + end + private # Called by +instantiate+ to decide which class to use for a new # record instance. @@ -174,6 +224,14 @@ module ActiveRecord def discriminate_class_for_record(record) self end + + def _substitute_values(values) + values.map do |name, value| + attr = arel_attribute(name) + bind = predicate_builder.build_bind_attribute(name, value) + [attr, bind] + end + end end # Returns true if this object hasn't been saved yet -- that is, a record @@ -271,7 +329,7 @@ module ActiveRecord # callbacks or any <tt>:dependent</tt> association # options, use <tt>#destroy</tt>. def delete - _relation_for_itself.delete_all if persisted? + _delete_row if persisted? @destroyed = true freeze end @@ -320,9 +378,10 @@ module ActiveRecord # Any change to the attributes on either instance will affect both instances. # If you want to change the sti column as well, use #becomes! instead. def becomes(klass) - became = klass.new + became = klass.allocate + became.send(:initialize) became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database) + became.instance_variable_set("@mutations_from_database", @mutations_from_database ||= nil) became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) @@ -363,11 +422,7 @@ module ActiveRecord verify_readonly_attribute(name) public_send("#{name}=", value) - if has_changes_to_save? - save(validate: false) - else - true - end + save(validate: false) end # Updates the attributes of the model from the passed-in hash and saves the @@ -383,6 +438,7 @@ module ActiveRecord end alias update_attributes update + deprecate :update_attributes # Updates its receiver just like #update but calls #save! instead # of +save+, so an exception is raised if the record is invalid and saving will fail. @@ -396,6 +452,7 @@ module ActiveRecord end alias update_attributes! update! + deprecate :update_attributes! # Equivalent to <code>update_columns(name => value)</code>. def update_column(name, value) @@ -426,13 +483,16 @@ module ActiveRecord verify_readonly_attribute(key.to_s) end - updated_count = _relation_for_itself.update_all(attributes) + affected_rows = self.class._update_record( + attributes, + self.class.primary_key => id_in_database + ) attributes.each do |k, v| write_attribute_without_type_cast(k, v) end - updated_count == 1 + affected_rows == 1 end # Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1). @@ -605,35 +665,12 @@ module ActiveRecord MSG end - time ||= current_time_from_proper_timezone - attributes = timestamp_attributes_for_update_in_model - attributes.concat(names) - - unless attributes.empty? - changes = {} - - attributes.each do |column| - column = column.to_s - changes[column] = write_attribute(column, time) - end - - scope = _relation_for_itself - - if locking_enabled? - locking_column = self.class.locking_column - scope = scope.where(locking_column => read_attribute_before_type_cast(locking_column)) - changes[locking_column] = increment_lock - end - - clear_attribute_changes(changes.keys) - result = scope.update_all(changes) == 1 - - if !result && locking_enabled? - raise ActiveRecord::StaleObjectError.new(self, "touch") - end + attribute_names = timestamp_attributes_for_update_in_model + attribute_names |= names.map(&:to_s) - @_trigger_update_callback = result - result + unless attribute_names.empty? + affected_rows = _touch_row(attribute_names, time) + @_trigger_update_callback = affected_rows == 1 else true end @@ -646,19 +683,34 @@ module ActiveRecord end def destroy_row - relation_for_destroy.delete_all + _delete_row end - def relation_for_destroy - _relation_for_itself + def _delete_row + self.class._delete_record(self.class.primary_key => id_in_database) end - def _relation_for_itself - self.class.unscoped.where(self.class.primary_key => id) + def _touch_row(attribute_names, time) + time ||= current_time_from_proper_timezone + + attribute_names.each do |attr_name| + write_attribute(attr_name, time) + clear_attribute_change(attr_name) + end + + _update_row(attribute_names, "touch") + end + + def _update_row(attribute_names, attempted_action = "update") + self.class._update_record( + attributes_with_values(attribute_names), + self.class.primary_key => id_in_database + ) end def create_or_update(*args, &block) _raise_readonly_record_error if readonly? + return false if destroyed? result = new_record? ? _create_record(&block) : _update_record(*args, &block) result != false end @@ -666,26 +718,29 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def _update_record(attribute_names = self.attribute_names) - attributes_values = arel_attributes_with_values_for_update(attribute_names) - if attributes_values.empty? - rows_affected = 0 + attribute_names &= self.class.column_names + attribute_names = attributes_for_update(attribute_names) + + if attribute_names.empty? + affected_rows = 0 @_trigger_update_callback = true else - rows_affected = self.class.unscoped._update_record attributes_values, id, id_in_database - @_trigger_update_callback = rows_affected > 0 + affected_rows = _update_row(attribute_names) + @_trigger_update_callback = affected_rows == 1 end yield(self) if block_given? - rows_affected + affected_rows end # Creates a record with values matching those of the instance attributes # and returns its id. def _create_record(attribute_names = self.attribute_names) - attributes_values = arel_attributes_with_values_for_create(attribute_names) + attribute_names &= self.class.column_names + attributes_values = attributes_with_values_for_create(attribute_names) - new_id = self.class.unscoped.insert attributes_values + new_id = self.class._insert_record(attributes_values) self.id ||= new_id if self.class.primary_key @new_record = false diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 3d5babb8b7..28194c7c46 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -7,35 +7,31 @@ module ActiveRecord # Enable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def cache(&block) - if configurations.empty? - yield - else + if connected? || !configurations.empty? connection.cache(&block) + else + yield end end # Disable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def uncached(&block) - if configurations.empty? - yield - else + if connected? || !configurations.empty? connection.uncached(&block) + else + yield end end end def self.run - caching_pool = ActiveRecord::Base.connection_pool - caching_was_enabled = caching_pool.query_cache_enabled - - caching_pool.enable_query_cache! - - [caching_pool, caching_was_enabled] + ActiveRecord::Base.connection_handler.connection_pool_list. + reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! } end - def self.complete((caching_pool, caching_was_enabled)) - caching_pool.disable_query_cache! unless caching_was_enabled + def self.complete(pools) + pools.each { |pool| pool.disable_query_cache! } ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool| pool.release_connection if pool.active_connection? && !pool.connection.transaction_open? diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 3996d5661f..c84f3d0fbb 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -5,7 +5,7 @@ module ActiveRecord delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all - delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all + delegate :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all delegate :destroy_all, :delete_all, :update_all, to: :all delegate :find_each, :find_in_batches, :in_batches, to: :all @@ -13,7 +13,7 @@ module ActiveRecord :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all - delegate :pluck, :ids, to: :all + delegate :pluck, :pick, :ids, to: :all # Executes a custom SQL query against your database and returns all the results. The results will # be returned as an array with columns requested encapsulated as attributes of the model you call @@ -49,7 +49,12 @@ module ActiveRecord } message_bus.instrument("instantiation.active_record", payload) do - result_set.map { |record| instantiate(record, column_types, &block) } + if result_set.includes_column?(inheritance_column) + result_set.map { |record| instantiate(record, column_types, &block) } + else + # Instantiate a homogeneous set + result_set.map { |record| instantiate_instance_of(self, record, column_types, &block) } + end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 812e1d7a00..7ece083fd4 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -59,6 +59,7 @@ module ActiveRecord console = ActiveSupport::Logger.new(STDERR) Rails.logger.extend ActiveSupport::Logger.broadcast console end + ActiveRecord::Base.verbose_query_logs = false end runner do @@ -76,6 +77,10 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end + initializer "active_record.backtrace_cleaner" do + ActiveSupport.on_load(:active_record) { LogSubscriber.backtrace_cleaner = ::Rails.backtrace_cleaner } + end + initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load config.app_middleware.insert_after ::ActionDispatch::Callbacks, @@ -90,12 +95,16 @@ module ActiveRecord filename = File.join(app.config.paths["db"].first, "schema_cache.yml") if File.file?(filename) + current_version = ActiveRecord::Migrator.current_version + + next if current_version.nil? + cache = YAML.load(File.read(filename)) - if cache.version == ActiveRecord::Migrator.current_version + if cache.version == current_version connection.schema_cache = cache connection_pool.schema_cache = cache.dup else - warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}." + warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{current_version}, but the one in the cache is #{cache.version}." end end end @@ -136,8 +145,8 @@ Oops - You have a database configured, but it doesn't exist yet! Here's how to get started: 1. Configure your database in config/database.yml. - 2. Run `bin/rails db:create` to create the database. - 3. Run `bin/rails db:setup` to load your database schema. + 2. Run `rails db:create` to create the database. + 3. Run `rails db:setup` to load your database schema. end_warning raise end @@ -152,6 +161,13 @@ end_warning end end + initializer "active_record.collection_cache_association_loading" do + require "active_record/railties/collection_cache_association_loading" + ActiveSupport.on_load(:action_view) do + ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading) + end + end + initializer "active_record.set_reloader_hooks" do ActiveSupport.on_load(:active_record) do ActiveSupport::Reloader.before_class_unload do @@ -177,7 +193,16 @@ end_warning initializer "active_record.clear_active_connections" do config.after_initialize do ActiveSupport.on_load(:active_record) do + # Ideally the application doesn't connect to the database during boot, + # but sometimes it does. In case it did, we want to empty out the + # connection pools so that a non-database-using process (e.g. a master + # process in a forking server model) doesn't retain a needless + # connection. If it was needed, the incremental cost of reestablishing + # this connection is trivial: the rest of the pool would need to be + # populated anyway. + clear_active_connections! + flush_idle_connections! end end end diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb new file mode 100644 index 0000000000..b5129e4239 --- /dev/null +++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module ActiveRecord + module Railties # :nodoc: + module CollectionCacheAssociationLoading #:nodoc: + def setup(context, options, block) + @relation = relation_from_options(options) + + super + end + + def relation_from_options(cached: nil, partial: nil, collection: nil, **_) + return unless cached + + relation = partial if partial.is_a?(ActiveRecord::Relation) + relation ||= collection if collection.is_a?(ActiveRecord::Relation) + + if relation && !relation.loaded? + relation.skip_preloading! + end + end + + def collection_without_template + @relation.preload_associations(@collection) if @relation + super + end + + def collection_with_template + @relation.preload_associations(@collection) if @relation + super + end + end + end +end diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index 2ae733f657..309441a057 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -8,49 +8,44 @@ module ActiveRecord module ControllerRuntime #:nodoc: extend ActiveSupport::Concern - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_internal :db_runtime - - private - - def process_action(action, *args) - # We also need to reset the runtime before each action - # because of queries in middleware or in cases we are streaming - # and it won't be cleaned up by the method below. - ActiveRecord::LogSubscriber.reset_runtime - super + module ClassMethods # :nodoc: + def log_process_action(payload) + messages, db_runtime = super, payload[:db_runtime] + messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime + messages + end end - def cleanup_view_runtime - if logger && logger.info? && ActiveRecord::Base.connected? - db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime - self.db_runtime = (db_runtime || 0) + db_rt_before_render - runtime = super - db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime - self.db_runtime += db_rt_after_render - runtime - db_rt_after_render - else + private + attr_internal :db_runtime + + def process_action(action, *args) + # We also need to reset the runtime before each action + # because of queries in middleware or in cases we are streaming + # and it won't be cleaned up by the method below. + ActiveRecord::LogSubscriber.reset_runtime super end - end - def append_info_to_payload(payload) - super - if ActiveRecord::Base.connected? - payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime + def cleanup_view_runtime + if logger && logger.info? && ActiveRecord::Base.connected? + db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime + self.db_runtime = (db_runtime || 0) + db_rt_before_render + runtime = super + db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime + self.db_runtime += db_rt_after_render + runtime - db_rt_after_render + else + super + end end - end - module ClassMethods # :nodoc: - def log_process_action(payload) - messages, db_runtime = super, payload[:db_runtime] - messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime - messages + def append_info_to_payload(payload) + super + if ActiveRecord::Base.connected? + payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime + end end - end end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 3bca2982e0..8b7d18fb3d 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -4,16 +4,16 @@ require "active_record" db_namespace = namespace :db do desc "Set the environment value for the database" - task "environment:set" => [:environment, :load_config] do + task "environment:set" => :load_config do ActiveRecord::InternalMetadata.create_table - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment end - task check_protected_environments: [:environment, :load_config] do + task check_protected_environments: :load_config do ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! end - task :load_config do + task load_config: :environment do ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {} ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths end @@ -22,6 +22,14 @@ db_namespace = namespace :db do task all: :load_config do ActiveRecord::Tasks::DatabaseTasks.create_all end + + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Create #{spec_name} database for current environment" + task spec_name => :load_config do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) + end + end end desc "Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases." @@ -33,6 +41,14 @@ db_namespace = namespace :db do task all: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.drop_all end + + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Drop #{spec_name} database for current environment" + task spec_name => [:load_config, :check_protected_environments] do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) + end + end end desc "Drops 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 or when RAILS_ENV is development, it defaults to dropping the development and test databases." @@ -56,8 +72,11 @@ db_namespace = namespace :db do end desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." - task migrate: [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.migrate + task migrate: :load_config do + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Tasks::DatabaseTasks.migrate + end db_namespace["_dump"].invoke end @@ -77,8 +96,17 @@ db_namespace = namespace :db do end namespace :migrate do + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Migrate #{spec_name} database for current environment" + task spec_name => :load_config do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end + # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' - task redo: [:environment, :load_config] do + task redo: :load_config do raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? if ENV["VERSION"] @@ -94,35 +122,33 @@ db_namespace = namespace :db do task reset: ["db:drop", "db:create", "db:migrate"] # desc 'Runs the "up" for a given migration VERSION.' - task up: [:environment, :load_config] do + task up: :load_config do raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? ActiveRecord::Tasks::DatabaseTasks.check_target_version - ActiveRecord::Migrator.run( + ActiveRecord::Base.connection.migration_context.run( :up, - ActiveRecord::Tasks::DatabaseTasks.migrations_paths, ActiveRecord::Tasks::DatabaseTasks.target_version ) db_namespace["_dump"].invoke end # desc 'Runs the "down" for a given migration VERSION.' - task down: [:environment, :load_config] do + task down: :load_config do raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty? ActiveRecord::Tasks::DatabaseTasks.check_target_version - ActiveRecord::Migrator.run( + ActiveRecord::Base.connection.migration_context.run( :down, - ActiveRecord::Tasks::DatabaseTasks.migrations_paths, ActiveRecord::Tasks::DatabaseTasks.target_version ) db_namespace["_dump"].invoke end desc "Display status of migrations" - task status: [:environment, :load_config] do + task status: :load_config do unless ActiveRecord::SchemaMigration.table_exists? abort "Schema migrations table does not exist yet." end @@ -131,8 +157,7 @@ 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 - paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths - ActiveRecord::Migrator.migrations_status(paths).each do |status, version, name| + ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name| puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts @@ -140,16 +165,16 @@ db_namespace = namespace :db do end desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)." - task rollback: [:environment, :load_config] do + task rollback: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 - ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + ActiveRecord::Base.connection.migration_context.rollback(step) db_namespace["_dump"].invoke end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' - task forward: [:environment, :load_config] do + task forward: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 - ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + ActiveRecord::Base.connection.migration_context.forward(step) db_namespace["_dump"].invoke end @@ -157,12 +182,12 @@ db_namespace = namespace :db do task reset: [ "db:drop", "db:setup" ] # desc "Retrieves the charset for the current environment's database" - task charset: [:environment, :load_config] do + task charset: :load_config do puts ActiveRecord::Tasks::DatabaseTasks.charset_current end # desc "Retrieves the collation for the current environment's database" - task collation: [:environment, :load_config] do + task collation: :load_config do begin puts ActiveRecord::Tasks::DatabaseTasks.collation_current rescue NoMethodError @@ -171,13 +196,13 @@ db_namespace = namespace :db do end desc "Retrieves the current schema version number" - task version: [:environment, :load_config] do - puts "Current version: #{ActiveRecord::Migrator.current_version}" + task version: :load_config do + puts "Current version: #{ActiveRecord::Base.connection.migration_context.current_version}" end # desc "Raises an error if there are pending migrations" - task abort_if_pending_migrations: [:environment, :load_config] do - pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations + task abort_if_pending_migrations: :load_config do + pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations if pending_migrations.any? puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" @@ -192,14 +217,14 @@ db_namespace = namespace :db do task setup: ["db:schema:load_if_ruby", "db:structure:load_if_sql", :seed] desc "Loads the seed data from db/seeds.rb" - task :seed do + task seed: :load_config do db_namespace["abort_if_pending_migrations"].invoke ActiveRecord::Tasks::DatabaseTasks.load_seed end namespace :fixtures do desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task load: [:environment, :load_config] do + task load: :load_config do require "active_record/fixtures" base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path @@ -221,7 +246,7 @@ db_namespace = namespace :db do end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task identify: [:environment, :load_config] do + task identify: :load_config do require "active_record/fixtures" label, id = ENV["LABEL"], ENV["ID"] @@ -232,7 +257,7 @@ db_namespace = namespace :db do base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path Dir["#{base_dir}/**/*.yml"].each do |file| - if data = YAML::load(ERB.new(IO.read(file)).result) + if data = YAML.load(ERB.new(IO.read(file)).result) data.each_key do |key| key_id = ActiveRecord::FixtureSet.identify(key) @@ -247,17 +272,22 @@ db_namespace = namespace :db do namespace :schema do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" - task dump: [:environment, :load_config] do + task dump: :load_config do require "active_record/schema_dumper" - filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema.rb") - File.open(filename, "w:utf-8") do |file| - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby) + File.open(filename, "w:utf-8") do |file| + ActiveRecord::Base.establish_connection(config) + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + end end + db_namespace["schema:dump"].reenable end desc "Loads a schema.rb file into the database" - task load: [:environment, :load_config, :check_protected_environments] do + task load: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV["SCHEMA"]) end @@ -267,39 +297,41 @@ db_namespace = namespace :db do namespace :cache do desc "Creates a db/schema_cache.yml file." - task dump: [:environment, :load_config] do + task dump: :load_config do conn = ActiveRecord::Base.connection filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(conn, filename) end desc "Clears a db/schema_cache.yml file." - task clear: [:environment, :load_config] do + task clear: :load_config do filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") rm_f filename, verbose: false end end - end namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" - task dump: [:environment, :load_config] do - 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) - - if ActiveRecord::SchemaMigration.table_exists? - File.open(filename, "a") do |f| - f.puts ActiveRecord::Base.connection.dump_schema_information - f.print "\n" + task dump: :load_config do + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::Base.establish_connection(config) + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename) + + if ActiveRecord::SchemaMigration.table_exists? + File.open(filename, "a") do |f| + f.puts ActiveRecord::Base.connection.dump_schema_information + f.print "\n" + end end end + db_namespace["structure:dump"].reenable end desc "Recreates the databases from the structure.sql file" - task load: [:environment, :load_config, :check_protected_environments] do + task load: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV["SCHEMA"]) end @@ -338,12 +370,12 @@ db_namespace = namespace :db do end # desc "Empty the test database" - task purge: %w(environment load_config check_protected_environments) do + task purge: %w(load_config check_protected_environments) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"] end # desc 'Load the test schema' - task prepare: %w(environment load_config) do + task prepare: :load_config do unless ActiveRecord::Base.configurations.blank? db_namespace["test:load"].invoke end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 87bfd75bca..6d2f75a3ae 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -13,32 +13,37 @@ module ActiveRecord class_attribute :aggregate_reflections, instance_writer: false, default: {} end - def self.create(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 + class << self + def create(macro, name, scope, options, ar) + reflection = reflection_class_for(macro).new(name, scope, options, ar) + options[:through] ? ThroughReflection.new(reflection) : reflection + end - reflection = klass.new(name, scope, options, ar) - options[:through] ? ThroughReflection.new(reflection) : reflection - end + def add_reflection(ar, name, reflection) + ar.clear_reflections_cache + name = name.to_s + ar._reflections = ar._reflections.except(name).merge!(name => reflection) + end - def self.add_reflection(ar, name, reflection) - ar.clear_reflections_cache - ar._reflections = ar._reflections.merge(name.to_s => reflection) - end + def add_aggregate_reflection(ar, name, reflection) + ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) + end - def self.add_aggregate_reflection(ar, name, reflection) - ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) + private + def reflection_class_for(macro) + case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end + end end # \Reflection enables the ability to examine the associations and aggregations of @@ -192,7 +197,7 @@ module ActiveRecord klass_scope = klass_join_scope(table, predicate_builder) if type - klass_scope.where!(type => foreign_klass.base_class.sti_name) + klass_scope.where!(type => foreign_klass.polymorphic_name) end scope_chain_items.inject(klass_scope, &:merge!) @@ -290,10 +295,14 @@ module ActiveRecord end def build_scope(table, predicate_builder = predicate_builder(table)) - Relation.create(klass, table, predicate_builder) + Relation.create( + klass, + table: table, + predicate_builder: predicate_builder + ) end - def join_primary_key(_) + def join_primary_key(*) foreign_key end @@ -411,6 +420,9 @@ module ActiveRecord # Active Record class. class AssociationReflection < MacroReflection #:nodoc: def compute_class(name) + if polymorphic? + raise ArgumentError, "Polymorphic associations do not support computing the class." + end active_record.send(:compute_type, name) end @@ -458,10 +470,6 @@ module ActiveRecord options[:primary_key] || primary_key(klass || self.klass) end - def association_primary_key_type - klass.type_for_attribute(association_primary_key.to_s) - end - def active_record_primary_key @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end @@ -567,7 +575,7 @@ module ActiveRecord end VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] - INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :foreign_key] + INVALID_AUTOMATIC_INVERSE_OPTIONS = [:through, :foreign_key] def add_as_source(seed) seed @@ -625,9 +633,6 @@ module ActiveRecord # +automatic_inverse_of+ method is a valid reflection. We must # make sure that the reflection's active_record name matches up # with the current reflection's klass name. - # - # Note: klass will always be valid because when there's a NameError - # from calling +klass+, +reflection+ will already be set to false. def valid_inverse_reflection?(reflection) reflection && klass <= reflection.active_record && @@ -722,7 +727,7 @@ module ActiveRecord end end - def join_primary_key(klass) + def join_primary_key(klass = nil) polymorphic? ? association_primary_key(klass) : association_primary_key end @@ -731,6 +736,9 @@ module ActiveRecord end private + def can_find_inverse_of_automatically?(_) + !polymorphic? && super + end def calculate_constructable(macro, options) !polymorphic? @@ -859,10 +867,6 @@ module ActiveRecord actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass) end - def association_primary_key_type - klass.type_for_attribute(association_primary_key.to_s) - end - # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form. # # class Post < ActiveRecord::Base @@ -965,16 +969,14 @@ module ActiveRecord collect_join_reflections(seed + [self]) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :delegate_reflection - def actual_source_reflection # FIXME: this is a horrible name source_reflection.actual_source_reflection end private + attr_reader :delegate_reflection + def collect_join_reflections(seed) a = source_reflection.add_as_source seed if options[:source_type] diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index e2d2f45503..2d3e1eaa08 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -9,8 +9,9 @@ module ActiveRecord SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :skip_query_cache] + CLAUSE_METHODS = [:where, :having, :from] - INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] + INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having] VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS @@ -18,17 +19,19 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded, :predicate_builder + attr_accessor :skip_preloading_value alias :model :klass alias :loaded? :loaded alias :locked? :lock_value - def initialize(klass, table, predicate_builder, values = {}) + def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {}) @klass = klass @table = table @values = values @offsets = {} @loaded = false @predicate_builder = predicate_builder + @delegate_to_klass = false end def initialize_copy(other) @@ -36,71 +39,16 @@ module ActiveRecord reset end - def insert(values) # :nodoc: - primary_key_value = nil - - if primary_key && Hash === values - primary_key_value = values[values.keys.find { |k| - k.name == primary_key - }] - - if !primary_key_value && klass.prefetch_primary_key? - primary_key_value = klass.next_sequence_value - values[arel_attribute(klass.primary_key)] = primary_key_value - end - end - - im = arel.create_insert - im.into @table - - substitutes = substitute_values values - - if values.empty? # empty insert - im.values = Arel.sql(connection.empty_insert_statement_value) - else - im.insert substitutes - end - - @klass.connection.insert( - im, - "#{@klass} Create", - primary_key || false, - primary_key_value, - nil, - ) - end - - def _update_record(values, id, id_was) # :nodoc: - substitutes = substitute_values values - - scope = @klass.unscoped - - if @klass.finder_needs_type_condition? - scope.unscope!(where: @klass.inheritance_column) - end - - relation = scope.where(@klass.primary_key => (id_was || id)) - um = relation - .arel - .compile_update(substitutes, @klass.primary_key) - - @klass.connection.update( - um, - "#{@klass} Update", - ) - end - - def substitute_values(values) # :nodoc: - values.map do |arel_attr, value| - bind = predicate_builder.build_bind_attribute(arel_attr.name, value) - [arel_attr, bind] - end - end - def arel_attribute(name) # :nodoc: klass.arel_attribute(name, table) end + def bind_attribute(name, value) # :nodoc: + attr = arel_attribute(name) + bind = predicate_builder.build_bind_attribute(attr.name, value) + yield attr, bind + end + # Initializes new record from relation while maintaining the current # scope. # @@ -113,8 +61,8 @@ module ActiveRecord # # user = users.new { |user| user.name = 'Oscar' } # user.name # => Oscar - def new(*args, &block) - scoping { @klass.new(*args, &block) } + def new(attributes = nil, &block) + scoping { klass.new(scope_for_create(attributes), &block) } end alias build new @@ -138,8 +86,12 @@ module ActiveRecord # # users.create(name: nil) # validation on name # # => #<User id: nil, name: nil, ...> - def create(*args, &block) - scoping { @klass.create(*args, &block) } + def create(attributes = nil, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr, &block) } + else + scoping { klass.create(scope_for_create(attributes), &block) } + end end # Similar to #create, but calls @@ -148,8 +100,12 @@ module ActiveRecord # # Expects arguments in the same format as # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]. - def create!(*args, &block) - scoping { @klass.create!(*args, &block) } + def create!(attributes = nil, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create!(attr, &block) } + else + scoping { klass.create!(scope_for_create(attributes), &block) } + end end def first_or_create(attributes = nil, &block) # :nodoc: @@ -195,23 +151,12 @@ module ActiveRecord # failed due to validation errors it won't be persisted, you get what # #create returns in such situation. # - # Please note *this method is not atomic*, it runs first a SELECT, and if + # Please note <b>this method is not atomic</b>, it runs first a SELECT, and if # there are no results an INSERT is attempted. If there are other threads # or processes there is a race condition between both calls and it could # be the case that you end up with two similar records. # - # Whether that is a problem or not depends on the logic of the - # application, but in the particular case in which rows have a UNIQUE - # constraint an exception may be raised, just retry: - # - # begin - # CreditAccount.transaction(requires_new: true) do - # CreditAccount.find_or_create_by(user_id: user.id) - # end - # rescue ActiveRecord::RecordNotUnique - # retry - # end - # + # If this might be a problem for your application, please see #create_or_find_by. def find_or_create_by(attributes, &block) find_by(attributes) || create(attributes, &block) end @@ -223,6 +168,47 @@ module ActiveRecord find_by(attributes) || create!(attributes, &block) end + # Attempts to create a record with the given attributes in a table that has a unique constraint + # on one or several of its columns. If a row already exists with one or several of these + # unique constraints, the exception such an insertion would normally raise is caught, + # and the existing record with those attributes is found using #find_by. + # + # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT + # and the INSERT, as that method needs to first query the table, then attempt to insert a row + # if none is found. + # + # There are several drawbacks to #create_or_find_by, though: + # + # * The underlying table must have the relevant columns defined with unique constraints. + # * A unique constraint violation may be triggered by only one, or at least less than all, + # of the given attributes. This means that the subsequent #find_by may fail to find a + # matching record, which will then raise an <tt>ActiveRecord::RecordNotFound</tt> exception, + # rather than a record with the given attributes. + # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by, + # we actually have another race condition between INSERT -> SELECT, which can be triggered + # if a DELETE between those two statements is run by another client. But for most applications, + # that's a significantly less likely condition to hit. + # * It relies on exception handling to handle control flow, which may be marginally slower. + # + # This method will return a record if all given attributes are covered by unique constraints + # (unless the INSERT -> DELETE -> SELECT race condition is triggered), but if creation was attempted + # and failed due to validation errors it won't be persisted, you get what #create returns in + # such situation. + def create_or_find_by(attributes, &block) + transaction(requires_new: true) { create(attributes, &block) } + rescue ActiveRecord::RecordNotUnique + find_by!(attributes) + end + + # Like #create_or_find_by, but calls + # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception + # is raised if the created record is invalid. + def create_or_find_by!(attributes, &block) + transaction(requires_new: true) { create!(attributes, &block) } + rescue ActiveRecord::RecordNotUnique + find_by!(attributes) + end + # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new] # instead of {create}[rdoc-ref:Persistence::ClassMethods#create]. def find_or_initialize_by(attributes, &block) @@ -237,7 +223,7 @@ module ActiveRecord # are needed by the next ones when eager loading is going on. # # Please see further details in the - # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. + # {Active Record Query Interface guide}[https://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain exec_explain(collecting_queries_for_explain { exec_queries }) end @@ -329,10 +315,17 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - previous, klass.current_scope = klass.current_scope(true), self + previous, klass.current_scope = klass.current_scope(true), self unless @delegate_to_klass yield ensure - klass.current_scope = previous + klass.current_scope = previous unless @delegate_to_klass + end + + def _exec_scope(*args, &block) # :nodoc: + @delegate_to_klass = true + instance_exec(*args, &block) || self + ensure + @delegate_to_klass = false end # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE @@ -367,10 +360,10 @@ module ActiveRecord stmt = Arel::UpdateManager.new - stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.set Arel.sql(@klass.sanitize_sql_for_assignment(updates)) stmt.table(table) - if has_join_values? + if has_join_values? || offset_value @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key)) else stmt.key = arel_attribute(primary_key) @@ -382,6 +375,62 @@ module ActiveRecord @klass.connection.update stmt, "#{@klass} Update All" end + def update(attributes) # :nodoc: + each { |record| record.update(attributes) } + end + + def update_counters(counters) # :nodoc: + touch = counters.delete(:touch) + + updates = counters.map do |counter_name, value| + operator = value < 0 ? "-" : "+" + quoted_column = connection.quote_column_name(counter_name) + "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + end + + if touch + names = touch if touch != true + touch_updates = klass.touch_attributes_with_time(*names) + updates << klass.sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty? + end + + update_all updates.join(", ") + end + + # Touches all records in the current relation without instantiating records first with the updated_at/on attributes + # set to the current time or the time specified. + # This method can be passed attribute names and an optional time argument. + # If attribute names are passed, they are updated along with updated_at/on attributes. + # If no time argument is passed, the current time is used as default. + # + # === Examples + # + # # Touch all records + # Person.all.touch_all + # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670'" + # + # # Touch multiple records with a custom attribute + # Person.all.touch_all(:created_at) + # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670', \"created_at\" = '2018-01-04 22:55:23.132670'" + # + # # Touch multiple records with a specified time + # Person.all.touch_all(time: Time.new(2020, 5, 16, 0, 0, 0)) + # # => "UPDATE \"people\" SET \"updated_at\" = '2020-05-16 00:00:00'" + # + # # Touch records with scope + # Person.where(name: 'David').touch_all + # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'" + def touch_all(*names, time: nil) + updates = touch_attributes_with_time(*names, time: time) + + if klass.locking_enabled? + quoted_locking_column = connection.quote_column_name(klass.locking_column) + updates = sanitize_sql_for_assignment(updates) + ", #{quoted_locking_column} = COALESCE(#{quoted_locking_column}, 0) + 1" + end + + update_all(updates) + end + # Destroys the records by instantiating each # record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method. # Each object's callbacks are executed (including <tt>:dependent</tt> association options). @@ -418,8 +467,8 @@ module ActiveRecord # # If an invalid method is supplied, #delete_all raises an ActiveRecordError: # - # Post.limit(100).delete_all - # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit + # Post.distinct.delete_all + # # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct def delete_all invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method| value = get_value(method) @@ -437,7 +486,7 @@ module ActiveRecord stmt = Arel::DeleteManager.new stmt.from(table) - if has_join_values? + if has_join_values? || has_limit_or_offset? @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key)) else stmt.wheres = arel.constraints @@ -468,6 +517,7 @@ module ActiveRecord end def reset + @delegate_to_klass = false @to_sql = @arel = @loaded = @should_eager_load = nil @records = [].freeze @offsets = {} @@ -480,17 +530,16 @@ module ActiveRecord # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= begin - relation = self - - if eager_loading? - find_with_associations { |rel, _| relation = rel } - end - - conn = klass.connection - conn.unprepared_statement { - conn.to_sql(relation.arel) - } - end + if eager_loading? + apply_join_dependency do |relation, join_dependency| + relation = join_dependency.apply_column_aliases(relation) + relation.to_sql + end + else + conn = klass.connection + conn.unprepared_statement { conn.to_sql(arel) } + end + end end # Returns a hash of where conditions. @@ -501,8 +550,10 @@ module ActiveRecord where_clause.to_h(relation_table_name) end - def scope_for_create - where_values_hash.merge!(create_with_value.stringify_keys) + def scope_for_create(attributes = nil) + scope = where_values_hash.merge!(create_with_value.stringify_keys) + scope.merge!(attributes) if attributes + scope end # Returns true if relation needs eager loading. @@ -567,6 +618,16 @@ module ActiveRecord ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins) end + def preload_associations(records) # :nodoc: + preload = preload_values + preload += includes_values unless eager_loading? + preloader = nil + preload.each do |associations| + preloader ||= build_preloader + preloader.preload records, associations + end + end + protected def load_records(records) @@ -584,10 +645,11 @@ module ActiveRecord skip_query_cache_if_necessary do @records = if eager_loading? - find_with_associations do |relation, join_dependency| + apply_join_dependency do |relation, join_dependency| if ActiveRecord::NullRelation === relation [] else + relation = join_dependency.apply_column_aliases(relation) rows = connection.select_all(relation.arel, "SQL") join_dependency.instantiate(rows, &block) end.freeze @@ -596,13 +658,7 @@ module ActiveRecord klass.find_by_sql(arel, &block).freeze end - preload = preload_values - preload += includes_values unless eager_loading? - preloader = nil - preload.each do |associations| - preloader ||= build_preloader - preloader.preload @records, associations - end + preload_associations(@records) unless skip_preloading_value @records.each(&:readonly!) if readonly_value diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 561869017a..9c579843b1 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -251,25 +251,28 @@ module ActiveRecord end end - attr = Relation::QueryAttribute.new(primary_key, primary_key_offset, klass.type_for_attribute(primary_key)) - batch_relation = relation.where(arel_attribute(primary_key).gt(Arel::Nodes::BindParam.new(attr))) + batch_relation = relation.where( + bind_attribute(primary_key, primary_key_offset) { |attr, bind| attr.gt(bind) } + ) end end private def apply_limits(relation, start, finish) - if start - attr = Relation::QueryAttribute.new(primary_key, start, klass.type_for_attribute(primary_key)) - relation = relation.where(arel_attribute(primary_key).gteq(Arel::Nodes::BindParam.new(attr))) - end - if finish - attr = Relation::QueryAttribute.new(primary_key, finish, klass.type_for_attribute(primary_key)) - relation = relation.where(arel_attribute(primary_key).lteq(Arel::Nodes::BindParam.new(attr))) - end + relation = apply_start_limit(relation, start) if start + relation = apply_finish_limit(relation, finish) if finish relation end + def apply_start_limit(relation, start) + relation.where(bind_attribute(primary_key, start) { |attr, bind| attr.gteq(bind) }) + end + + def apply_finish_limit(relation, finish) + relation.where(bind_attribute(primary_key, finish) { |attr, bind| attr.lteq(bind) }) + end + def batch_order arel_attribute(primary_key).asc end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 11256ab3d9..40fe39fa9d 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -131,7 +131,14 @@ module ActiveRecord def calculate(operation, column_name) if has_include?(column_name) relation = apply_join_dependency - relation.distinct! if operation.to_s.downcase == "count" + + if operation.to_s.downcase == "count" + relation.distinct! + # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT + if (column_name == :all || column_name.nil?) && select_values.empty? + relation.order_values = [] + end + end relation.calculate(operation, column_name) else @@ -183,6 +190,7 @@ module ActiveRecord relation = apply_join_dependency relation.pluck(*column_names) else + enforce_raw_sql_whitelist(column_names) relation = spawn relation.select_values = column_names.map { |cn| @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn @@ -192,6 +200,24 @@ module ActiveRecord end end + # Pick the value(s) from the named column(s) in the current relation. + # This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful + # when you have a relation that's already narrowed down to a single row. + # + # Just like #pluck, #pick will only load the actual value, not the entire record object, so it's also + # more efficient. The value is, again like with pluck, typecast by the column type. + # + # Person.where(id: 1).pick(:name) + # # SELECT people.name FROM people WHERE id = 1 LIMIT 1 + # # => 'David' + # + # Person.where(id: 1).pick(:name, :email_address) + # # SELECT people.name, people.email_address FROM people WHERE id = 1 LIMIT 1 + # # => [ 'David', 'david@loudthinking.com' ] + def pick(*column_names) + limit(1).pluck(*column_names).first + end + # Pluck all the ID's for the relation using the table's primary key # # Person.ids # SELECT people.id FROM people @@ -216,10 +242,10 @@ module ActiveRecord if operation == "count" column_name ||= select_for_count if column_name == :all - if distinct && (group_values.any? || !(has_limit_or_offset? && order_values.any?)) + if distinct && (group_values.any? || select_values.empty? && order_values.empty?) column_name = primary_key end - elsif column_name =~ /\s*DISTINCT[\s(]+/i + elsif /\s*DISTINCT[\s(]+/i.match?(column_name.to_s) distinct = nil end end @@ -234,7 +260,7 @@ module ActiveRecord def aggregate_column(column_name) return column_name if Arel::Expressions === column_name - if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s) + if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name) @klass.arel_attribute(column_name) else Arel.sql(column_name == :all ? "*" : column_name.to_s) @@ -248,7 +274,7 @@ module ActiveRecord def execute_simple_calculation(operation, column_name, distinct) #:nodoc: column_alias = column_name - if operation == "count" && has_limit_or_offset? + if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?) # Shortcut when limit is zero. return 0 if limit_value == 0 @@ -390,14 +416,12 @@ module ActiveRecord end def build_count_subquery(relation, column_name, distinct) - relation.select_values = [ - if column_name == :all - distinct ? table[Arel.star] : Arel.sql(FinderMethods::ONE_AS_ONE) - else - column_alias = Arel.sql("count_column") - aggregate_column(column_name).as(column_alias) - end - ] + if column_name == :all + relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct + else + column_alias = Arel.sql("count_column") + relation.select_values = [ aggregate_column(column_name).as(column_alias) ] + end subquery = relation.arel.as(Arel.sql("subquery_for_count")) select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false) diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 4863befec8..488f71cdde 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -38,7 +38,7 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - delegate :to_xml, :encode_with, :length, :each, :uniq, :join, + delegate :to_xml, :encode_with, :length, :each, :join, :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of, :to_sentence, :to_formatted_s, :as_json, :shuffle, :split, :slice, :index, :rindex, to: :records @@ -82,6 +82,11 @@ module ActiveRecord if @klass.respond_to?(method) self.class.delegate_to_scoped_klass(method) scoping { @klass.public_send(method, *args, &block) } + elsif @delegate_to_klass && @klass.respond_to?(method, true) + ActiveSupport::Deprecation.warn \ + "Delegating missing #{method} method to #{@klass}. " \ + "Accessibility of private/protected class methods in :scope is deprecated and will be removed in Rails 6.0." + @klass.send(method, *args, &block) elsif arel.respond_to?(method) ActiveSupport::Deprecation.warn \ "Delegating #{method} to arel is deprecated and will be removed in Rails 6.0." diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 18566b5662..c5562c1ff0 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -18,9 +18,10 @@ module ActiveRecord # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # - # NOTE: The returned records may not be in the same order as the ids you - # provide since database rows are unordered. You will need to provide an explicit QueryMethods#order - # option if you want the results to be sorted. + # NOTE: The returned records are in the same order as the ids you provide. + # If you want the results to be sorted by database, you can use ActiveRecord::QueryMethods#where + # method and provide an explicit ActiveRecord::QueryMethods#order option. + # But ActiveRecord::QueryMethods#where method doesn't raise ActiveRecord::RecordNotFound. # # ==== Find with lock # @@ -88,7 +89,7 @@ module ActiveRecord where(arg, *args).take! rescue ::RangeError raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", - @klass.name) + @klass.name, @klass.primary_key) end # Gives a record (or N records if a parameter is supplied) without any implied @@ -147,7 +148,7 @@ module ActiveRecord # # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) - return find_last(limit) if loaded? || limit_value + return find_last(limit) if loaded? || has_limit_or_offset? result = ordered_relation.limit(limit) result = result.reverse_order! @@ -284,7 +285,7 @@ module ActiveRecord # * Hash - Finds the record that matches these +find+-style conditions # (such as <tt>{name: 'David'}</tt>). # * +false+ - Returns always +false+. - # * No args - Returns +false+ if the table is empty, +true+ otherwise. + # * No args - Returns +false+ if the relation is empty, +true+ otherwise. # # For more information about specifying conditions as a hash or array, # see the Conditions section in the introduction to ActiveRecord::Base. @@ -300,6 +301,7 @@ module ActiveRecord # Person.exists?(name: 'David') # Person.exists?(false) # Person.exists? + # Person.where(name: 'Spartacus', rating: 4).exists? def exists?(conditions = :none) if Base === conditions raise ArgumentError, <<-MSG.squish @@ -311,7 +313,7 @@ module ActiveRecord return false if !conditions || limit_value == 0 if eager_loading? - relation = apply_join_dependency(construct_join_dependency(eager_loading: false)) + relation = apply_join_dependency(eager_loading: false) return relation.exists?(conditions) end @@ -338,7 +340,7 @@ module ActiveRecord if ids.nil? error = "Couldn't find #{name}".dup error << " with#{conditions}" if conditions - raise RecordNotFound.new(error, name) + raise RecordNotFound.new(error, name, key) elsif Array(ids).size == 1 error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}" raise RecordNotFound.new(error, name, key, ids) @@ -346,7 +348,7 @@ module ActiveRecord error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})." error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids - raise RecordNotFound.new(error, name, primary_key, ids) + raise RecordNotFound.new(error, name, key, ids) end end @@ -356,24 +358,6 @@ module ActiveRecord offset_value || 0 end - def find_with_associations - # NOTE: the JoinDependency constructed here needs to know about - # any joins already present in `self`, so pass them in - # - # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136 - # incorrect SQL is generated. In that case, the join dependency for - # SpecialCategorizations is constructed without knowledge of the - # preexisting join in joins_values to categorizations (by way of - # the `has_many :through` for categories). - # - join_dependency = construct_join_dependency - - relation = apply_join_dependency(join_dependency) - relation._select!(join_dependency.aliases.columns) - - yield relation, join_dependency - end - def construct_relation_for_exists(conditions) relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) @@ -387,24 +371,28 @@ module ActiveRecord relation end - def construct_join_dependency(eager_loading: true) - including = eager_load_values + includes_values + def construct_join_dependency(associations) ActiveRecord::Associations::JoinDependency.new( - klass, table, including, alias_tracker(joins_values), eager_loading: eager_loading + klass, table, associations ) end - def apply_join_dependency(join_dependency = construct_join_dependency) + def apply_join_dependency(eager_loading: group_values.empty?) + join_dependency = construct_join_dependency(eager_load_values + includes_values) relation = except(:includes, :eager_load, :preload).joins!(join_dependency) - if using_limitable_reflections?(join_dependency.reflections) - relation - else - if relation.limit_value + if eager_loading && !using_limitable_reflections?(join_dependency.reflections) + if has_limit_or_offset? limited_ids = limited_ids_for(relation) limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end - relation.except(:limit, :offset) + relation.limit_value = relation.offset_value = nil + end + + if block_given? + yield relation, join_dependency + else + relation end end @@ -432,9 +420,12 @@ module ActiveRecord ids = ids.flatten.compact.uniq + model_name = @klass.name + case ids.size when 0 - raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" + error_message = "Couldn't find #{model_name} without an ID" + raise RecordNotFound.new(error_message, model_name, primary_key) when 1 result = find_one(ids.first) expects_array ? [ result ] : result @@ -442,7 +433,8 @@ module ActiveRecord find_some(ids) end rescue ::RangeError - raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" + error_message = "Couldn't find #{model_name} with an out of range ID" + raise RecordNotFound.new(error_message, model_name, primary_key, ids) end def find_one(id) @@ -526,7 +518,11 @@ module ActiveRecord else relation = ordered_relation - if limit_value.nil? || index < limit_value + if limit_value + limit = [limit_value - index, limit].min + end + + if limit > 0 relation = relation.offset(offset_index + index) unless index.zero? relation.limit(limit).to_a else @@ -541,12 +537,11 @@ module ActiveRecord else relation = ordered_relation - relation.to_a[-index] - # TODO: can be made more performant on large result sets by - # for instance, last(index)[-index] (which would require - # refactoring the last(n) finder method to make test suite pass), - # or by using a combination of reverse_order, limit, and offset, - # e.g., reverse_order.offset(index-1).first + if equal?(relation) || has_limit_or_offset? + relation.records[-index] + else + relation.last(index)[-index] + end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index b736b21525..10e4779ca4 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -23,7 +23,11 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.create(relation.klass, relation.table, relation.predicate_builder) + other = Relation.create( + relation.klass, + table: relation.table, + predicate_builder: relation.predicate_builder + ) hash.each { |k, v| if k == :joins if Hash === v @@ -52,7 +56,7 @@ module ActiveRecord NORMAL_VALUES = Relation::VALUE_METHODS - Relation::CLAUSE_METHODS - - [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: + [:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: def normal_values NORMAL_VALUES @@ -79,6 +83,7 @@ module ActiveRecord merge_clauses merge_preloads merge_joins + merge_outer_joins relation end @@ -112,14 +117,10 @@ module ActiveRecord if other.klass == relation.klass relation.joins!(*other.joins_values) else - alias_tracker = nil joins_dependency = other.joins_values.map do |join| case join when Hash, Symbol, Array - alias_tracker ||= other.alias_tracker - ActiveRecord::Associations::JoinDependency.new( - other.klass, other.table, join, alias_tracker - ) + other.send(:construct_join_dependency, join) else join end @@ -129,6 +130,25 @@ module ActiveRecord end end + def merge_outer_joins + return if other.left_outer_joins_values.blank? + + if other.klass == relation.klass + relation.left_outer_joins!(*other.left_outer_joins_values) + else + joins_dependency = other.left_outer_joins_values.map do |join| + case join + when Hash, Symbol, Array + other.send(:construct_join_dependency, join) + else + join + end + end + + relation.left_outer_joins!(*joins_dependency) + end + end + def merge_multi_values if other.reordering_value # override any order specified in the original relation diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 885c26d7aa..f734cd0ad8 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -48,7 +48,12 @@ module ActiveRecord end def build(attribute, value) - handler_for(value).call(attribute, value) + if table.type(attribute.name).force_equality?(value) + bind = build_bind_attribute(attribute.name, value) + attribute.eq(bind) + else + handler_for(value).call(attribute, value) + end end def build_bind_attribute(column_name, value) @@ -57,9 +62,6 @@ module ActiveRecord end protected - - attr_reader :table - def expand_from_hash(attributes) return ["1=0"] if attributes.empty? @@ -86,10 +88,18 @@ module ActiveRecord expand_from_hash(query).reduce(&:and) end queries.reduce(&:or) - # FIXME: Deprecate this and provide a public API to force equality - elsif (value.is_a?(Range) || value.is_a?(Array)) && - table.type(key.to_s).respond_to?(:subtype) - BasicObjectHandler.new(self).call(table.arel_attribute(key), value) + elsif table.aggregated_with?(key) + mapping = table.reflect_on_aggregation(key).mapping + queries = Array.wrap(value).map do |object| + mapping.map do |field_attr, aggregate_attr| + if mapping.size == 1 && !object.respond_to?(aggregate_attr) + build(table.arel_attribute(field_attr), object) + else + build(table.arel_attribute(field_attr), object.send(aggregate_attr)) + end + end.reduce(&:and) + end + queries.reduce(&:or) else build(table.arel_attribute(key), value) end @@ -97,6 +107,7 @@ module ActiveRecord end private + attr_reader :table def associated_predicate_builder(association_name) self.class.new(table.associated_table(association_name)) 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 2fd75c8958..e5191fa38a 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/array/extract" + module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: @@ -11,8 +13,8 @@ module ActiveRecord return attribute.in([]) if value.empty? values = value.map { |x| x.is_a?(Base) ? x.id : x } - nils, values = values.partition(&:nil?) - ranges, values = values.partition { |v| v.is_a?(Range) } + nils = values.extract!(&:nil?) + ranges = values.extract! { |v| v.is_a?(Range) } values_predicate = case values.length @@ -34,8 +36,7 @@ module ActiveRecord array_predicates.inject(&:or) end - protected - + private attr_reader :predicate_builder module NullPredicate # :nodoc: diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb index 0255a65bfe..88cd71cf69 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb @@ -12,12 +12,9 @@ module ActiveRecord [associated_table.association_join_foreign_key.to_s => ids] end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected + private attr_reader :associated_table, :value - private def ids case value when Relation @@ -30,7 +27,7 @@ module ActiveRecord end def primary_key - associated_table.association_join_keys.key + associated_table.association_join_primary_key end def convert_to_id(value) diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb index 112821135f..10c5c1a66a 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -11,8 +11,7 @@ module ActiveRecord predicate_builder.build(attribute, value.id) end - protected - + private attr_reader :predicate_builder 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 index 34db266f05..e8c9f60860 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb @@ -12,8 +12,7 @@ module ActiveRecord attribute.eq(bind) end - protected - + private attr_reader :predicate_builder end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb index b87b5c36dd..aae04d9348 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -17,27 +17,26 @@ module ActiveRecord end end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected + private attr_reader :associated_table, :values - private def type_to_ids_mapping default_hash = Hash.new { |hsh, key| hsh[key] = [] } - values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } + values.each_with_object(default_hash) do |value, hash| + hash[klass(value).polymorphic_name] << convert_to_id(value) + end end def primary_key(value) - associated_table.association_primary_key(base_class(value)) + associated_table.association_join_primary_key(klass(value)) end - def base_class(value) + def klass(value) case value when Base - value.class.base_class + value.class when Relation - value.klass.base_class + value.klass 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 index 6d16579708..44bb2c7ab6 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -16,15 +16,16 @@ module ActiveRecord def call(attribute, value) begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin) end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end) - if value.begin.respond_to?(:infinite?) && value.begin.infinite? - if value.end.respond_to?(:infinite?) && value.end.infinite? + + if begin_bind.value.infinity? + if end_bind.value.infinity? attribute.not_in([]) elsif value.exclude_end? attribute.lt(end_bind) else attribute.lteq(end_bind) end - elsif value.end.respond_to?(:infinite?) && value.end.infinite? + elsif end_bind.value.infinity? attribute.gteq(begin_bind) elsif value.exclude_end? attribute.gteq(begin_bind).and(attribute.lt(end_bind)) @@ -33,8 +34,7 @@ module ActiveRecord end end - protected - + private attr_reader :predicate_builder end end diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index 3532f28858..f64bd30d38 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -21,6 +21,23 @@ module ActiveRecord !value_before_type_cast.is_a?(StatementCache::Substitute) && (value_before_type_cast.nil? || value_for_database.nil?) end + + def boundable? + return @_boundable if defined?(@_boundable) + nil? + @_boundable = true + rescue ::RangeError + @_boundable = false + end + + def infinity? + _infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database) + end + + private + def _infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + 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 34554450dd..52405f21a1 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -231,6 +231,7 @@ module ActiveRecord end def _select!(*fields) # :nodoc: + fields.reject!(&:blank?) fields.flatten! fields.map! do |field| klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field @@ -295,6 +296,7 @@ module ActiveRecord spawn.order!(*args) end + # Same as #order but operates on relation in-place instead of copying. def order!(*args) # :nodoc: preprocess_order_args(args) @@ -316,6 +318,7 @@ module ActiveRecord spawn.reorder!(*args) end + # Same as #reorder but operates on relation in-place instead of copying. def reorder!(*args) # :nodoc: preprocess_order_args(args) @@ -325,8 +328,8 @@ module ActiveRecord end VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock, - :limit, :offset, :joins, :includes, :from, - :readonly, :having]) + :limit, :offset, :joins, :left_outer_joins, + :includes, :from, :readonly, :having]) # Removes an unwanted relation that is already defined on a chain of relations. # This is useful when passing around chains of relations and would like to @@ -373,10 +376,11 @@ module ActiveRecord args.each do |scope| case scope when Symbol + scope = :left_outer_joins if scope == :left_joins if !VALID_UNSCOPING_VALUES.include?(scope) raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." end - set_value(scope, nil) + set_value(scope, DEFAULT_VALUES[scope]) when Hash scope.each do |key, target_value| if key != :where @@ -442,15 +446,13 @@ module ActiveRecord # def left_outer_joins(*args) check_if_method_has_arguments!(__callee__, args) - - args.compact! - args.flatten! - spawn.left_outer_joins!(*args) end alias :left_joins :left_outer_joins def left_outer_joins!(*args) # :nodoc: + args.compact! + args.flatten! self.left_outer_joins_values += args self end @@ -892,8 +894,13 @@ module ActiveRecord self end - def skip_query_cache! # :nodoc: - self.skip_query_cache_value = true + def skip_query_cache!(value = true) # :nodoc: + self.skip_query_cache_value = value + self + end + + def skip_preloading! # :nodoc: + self.skip_preloading_value = true self end @@ -902,11 +909,12 @@ module ActiveRecord @arel ||= build_arel(aliases) end + # Returns a relation value with a given name + def get_value(name) # :nodoc: + @values.fetch(name, DEFAULT_VALUES[name]) + end + protected - # Returns a relation value with a given name - def get_value(name) # :nodoc: - @values[name] || default_value_for(name) - end # Sets the relation value with the given name def set_value(name, value) # :nodoc: @@ -978,6 +986,8 @@ module ActiveRecord case join when Hash, Symbol, Array :association_join + when ActiveRecord::Associations::JoinDependency + :stashed_join else raise ArgumentError, "only Hash, Symbol and Array are allowed" end @@ -1008,19 +1018,17 @@ module ActiveRecord def build_join_query(manager, buckets, join_type, aliases) 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_joins = buckets[:stashed_join] + join_nodes = buckets[:join_node].uniq + string_joins = buckets[:string_join].map(&:strip).uniq - join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins) + join_list = join_nodes + convert_join_strings_to_ast(string_joins) alias_tracker = alias_tracker(join_list, aliases) - join_dependency = ActiveRecord::Associations::JoinDependency.new( - klass, table, association_joins, alias_tracker - ) + join_dependency = construct_join_dependency(association_joins) - joins = join_dependency.join_constraints(stashed_association_joins, join_type) + joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker) joins.each { |join| manager.from(join) } manager.join_sources.concat(join_list) @@ -1028,7 +1036,7 @@ module ActiveRecord alias_tracker.aliases end - def convert_join_strings_to_ast(table, joins) + def convert_join_strings_to_ast(joins) joins .flatten .reject(&:blank?) @@ -1038,17 +1046,21 @@ module ActiveRecord def build_select(arel) if select_values.any? arel.project(*arel_columns(select_values.uniq)) + elsif klass.ignored_columns.any? + arel.project(*klass.column_names.map { |field| arel_attribute(field) }) else arel.project(table[Arel.star]) end end def arel_columns(columns) - columns.map do |field| + columns.flat_map do |field| if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value arel_attribute(field) elsif Symbol === field connection.quote_table_name(field.to_s) + elsif Proc === field + field.call else field end @@ -1074,7 +1086,7 @@ module ActiveRecord end o.split(",").map! do |s| s.strip! - s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || s.concat(" DESC") + s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC") end else o @@ -1083,6 +1095,10 @@ module ActiveRecord end def does_not_support_reverse?(order) + # Account for String subclasses like Arel::Nodes::SqlLiteral that + # override methods like #count. + order = String.new(order) unless order.instance_of?(String) + # Uses SQL function with multiple arguments. (order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) || # Uses "nulls first" like construction. @@ -1113,9 +1129,15 @@ module ActiveRecord def preprocess_order_args(order_args) order_args.map! do |arg| - klass.send(:sanitize_sql_for_order, arg) + klass.sanitize_sql_for_order(arg) end order_args.flatten! + + @klass.enforce_raw_sql_whitelist( + order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a }, + whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + ) + validate_order_args(order_args) references = order_args.grep(String) @@ -1178,7 +1200,6 @@ module ActiveRecord DEFAULT_VALUES = { create_with: FROZEN_EMPTY_HASH, - readonly: false, where: Relation::WhereClause.empty, having: Relation::WhereClause.empty, from: Relation::FromClause.empty @@ -1187,15 +1208,5 @@ module ActiveRecord Relation::MULTI_VALUE_METHODS.each do |value| DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY end - - Relation::SINGLE_VALUE_METHODS.each do |value| - DEFAULT_VALUES[value] = nil if DEFAULT_VALUES[value].nil? - end - - def default_value_for(name) - DEFAULT_VALUES.fetch(name) do - raise ArgumentError, "unknown relation value #{name.inspect}" - end - end end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 617d8de8b2..562e04194c 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -69,7 +69,7 @@ module ActiveRecord private def relation_with(values) - result = Relation.create(klass, table, predicate_builder, values) + result = Relation.create(klass, values: 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 index 752bb38481..a502713e56 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -47,7 +47,7 @@ module ActiveRecord end def to_h(table_name = nil) - equalities = predicates.grep(Arel::Nodes::Equality) + equalities = equalities(predicates) if table_name equalities = equalities.select do |node| node.left.relation.name == table_name @@ -90,6 +90,20 @@ module ActiveRecord end private + def equalities(predicates) + equalities = [] + + predicates.each do |node| + case node + when Arel::Nodes::Equality + equalities << node + when Arel::Nodes::And + equalities.concat equalities(node.children) + end + end + + equalities + end def predicates_unreferenced_by(other) predicates.reject do |n| @@ -121,7 +135,7 @@ module ActiveRecord end def except_predicates(columns) - self.predicates.reject do |node| + predicates.reject do |node| case node when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 1374785354..c1b3eea9df 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -11,10 +11,9 @@ module ActiveRecord def build(opts, other) case opts when String, Array - parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] + parts = [klass.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.stringify_keys! parts = predicate_builder.build_from_hash(attributes) @@ -27,8 +26,7 @@ module ActiveRecord WhereClause.new(parts) end - protected - + private attr_reader :klass, :predicate_builder end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index e54e8086dd..7f1c2fd7eb 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -43,6 +43,11 @@ module ActiveRecord @column_types = column_types end + # Returns true if this result set includes the column named +name+ + def includes_column?(name) + @columns.include? name + end + # Returns the number of elements in the rows array. def length @rows.length @@ -97,12 +102,21 @@ module ActiveRecord 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 + if columns.one? + # Separated to avoid allocating an array per row - columns.one? ? result.map!(&:first) : result + type = column_type(columns.first, type_overrides) + + rows.map do |(value)| + type.deserialize(value) + end + else + types = columns.map { |name| column_type(name, type_overrides) } + + rows.map do |values| + Array.new(values.size) { |i| types[i].deserialize(values[i]) } + end + end end def initialize_copy(other) @@ -125,7 +139,7 @@ module ActiveRecord begin # We freeze the strings to prevent them getting duped when # used as keys in ActiveRecord::Base's @attributes hash - columns = @columns.map { |c| c.dup.freeze } + columns = @columns.map(&:-@) @rows.map { |row| # In the past we used Hash[columns.zip(row)] # though elegant, the verbose way is much more efficient diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 1c3099f55c..c6c268855e 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -5,70 +5,135 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - private + # Accepts an array or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a WHERE clause. + # + # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id='4'" + # + # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" + # + # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'") + # # => "name='foo''bar' and group_id='4'" + def sanitize_sql_for_conditions(condition) + return nil if condition.blank? - # Accepts an array or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a WHERE clause. - # - # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4]) - # # => "name='foo''bar' and group_id=4" - # - # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) - # # => "name='foo''bar' and group_id='4'" - # - # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4]) - # # => "name='foo''bar' and group_id='4'" - # - # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'") - # # => "name='foo''bar' and group_id='4'" - def sanitize_sql_for_conditions(condition) # :doc: - return nil if condition.blank? - - case condition - when Array; sanitize_sql_array(condition) - else condition - end + case condition + when Array; sanitize_sql_array(condition) + else condition end - alias :sanitize_sql :sanitize_sql_for_conditions + end + alias :sanitize_sql :sanitize_sql_for_conditions - # Accepts an array, hash, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a SET clause. - # - # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4]) - # # => "name=NULL and group_id=4" - # - # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4]) - # # => "name=NULL and group_id=4" - # - # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 }) - # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4" - # - # sanitize_sql_for_assignment("name=NULL and group_id='4'") - # # => "name=NULL and group_id='4'" - def sanitize_sql_for_assignment(assignments, default_table_name = table_name) # :doc: - case assignments - when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name) - else assignments - end + # Accepts an array, hash, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a SET clause. + # + # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4]) + # # => "name=NULL and group_id=4" + # + # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4]) + # # => "name=NULL and group_id=4" + # + # Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 }) + # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4" + # + # sanitize_sql_for_assignment("name=NULL and group_id='4'") + # # => "name=NULL and group_id='4'" + def sanitize_sql_for_assignment(assignments, default_table_name = table_name) + case assignments + when Array; sanitize_sql_array(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name) + else assignments end + end - # Accepts an array, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for an ORDER clause. - # - # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) - # # => "field(id, 1,3,2)" - # - # sanitize_sql_for_order("id ASC") - # # => "id ASC" - def sanitize_sql_for_order(condition) # :doc: - if condition.is_a?(Array) && condition.first.to_s.include?("?") - sanitize_sql_array(condition) - else - condition + # Accepts an array, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for an ORDER clause. + # + # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) + # # => "field(id, 1,3,2)" + # + # sanitize_sql_for_order("id ASC") + # # => "id ASC" + def sanitize_sql_for_order(condition) + if condition.is_a?(Array) && condition.first.to_s.include?("?") + enforce_raw_sql_whitelist([condition.first], + whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + ) + + # Ensure we aren't dealing with a subclass of String that might + # override methods we use (eg. Arel::Nodes::SqlLiteral). + if condition.first.kind_of?(String) && !condition.first.instance_of?(String) + condition = [String.new(condition.first), *condition[1..-1]] end + + Arel.sql(sanitize_sql_array(condition)) + else + condition + end + end + + # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. + # + # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts") + # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1" + def sanitize_sql_hash_for_assignment(attrs, table) + c = connection + attrs.map do |attr, value| + type = type_for_attribute(attr) + value = type.serialize(type.cast(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 an SQL + # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%". + # + # sanitize_sql_like("100%") + # # => "100\\%" + # + # sanitize_sql_like("snake_cased_string") + # # => "snake\\_cased\\_string" + # + # sanitize_sql_like("100%", "!") + # # => "100!%" + # + # sanitize_sql_like("snake_cased_string", "!") + # # => "snake!_cased!_string" + def sanitize_sql_like(string, escape_character = "\\") + pattern = Regexp.union(escape_character, "%", "_") + string.gsub(pattern) { |x| [escape_character, x].join } + end + + # Accepts an array of conditions. The array has each value + # sanitized and interpolated into the SQL statement. + # + # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" + def sanitize_sql_array(ary) + statement, *values = ary + if values.first.is_a?(Hash) && /:\w+/.match?(statement) + replace_named_bind_variables(statement, values.first) + elsif statement.include?("?") + replace_bind_variables(statement, values) + elsif statement.blank? + statement + else + statement % values.collect { |value| connection.quote_string(value.to_s) } end + end + private # Accepts a hash of SQL conditions and replaces those attributes # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of] # relationship with their expanded aggregate attribute values. @@ -90,10 +155,12 @@ module ActiveRecord if aggregation = reflect_on_aggregation(attr.to_sym) mapping = aggregation.mapping mapping.each do |field_attr, aggregate_attr| - if mapping.size == 1 && !value.respond_to?(aggregate_attr) - expanded_attrs[field_attr] = value + expanded_attrs[field_attr] = if value.is_a?(Array) + value.map { |it| it.send(aggregate_attr) } + elsif mapping.size == 1 && !value.respond_to?(aggregate_attr) + value else - expanded_attrs[field_attr] = value.send(aggregate_attr) + value.send(aggregate_attr) end end else @@ -102,61 +169,7 @@ module ActiveRecord end expanded_attrs end - - # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. - # - # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts") - # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1" - def sanitize_sql_hash_for_assignment(attrs, table) # :doc: - c = connection - attrs.map do |attr, value| - 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 an SQL - # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%". - # - # sanitize_sql_like("100%") - # # => "100\\%" - # - # sanitize_sql_like("snake_cased_string") - # # => "snake\\_cased\\_string" - # - # sanitize_sql_like("100%", "!") - # # => "100!%" - # - # sanitize_sql_like("snake_cased_string", "!") - # # => "snake!_cased!_string" - def sanitize_sql_like(string, escape_character = "\\") # :doc: - pattern = Regexp.union(escape_character, "%", "_") - string.gsub(pattern) { |x| [escape_character, x].join } - end - - # Accepts an array of conditions. The array has each value - # sanitized and interpolated into the SQL statement. - # - # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4]) - # # => "name='foo''bar' and group_id=4" - # - # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) - # # => "name='foo''bar' and group_id=4" - # - # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) - # # => "name='foo''bar' and group_id='4'" - def sanitize_sql_array(ary) # :doc: - statement, *values = ary - if values.first.is_a?(Hash) && /:\w+/.match?(statement) - replace_named_bind_variables(statement, values.first) - elsif statement.include?("?") - replace_bind_variables(statement, values) - elsif statement.blank? - statement - else - statement % values.collect { |value| connection.quote_string(value.to_s) } - end - end + deprecate :expand_hash_conditions_for_aggregates def replace_bind_variables(statement, values) raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size) diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 1e121f2a09..216359867c 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -55,7 +55,7 @@ module ActiveRecord end ActiveRecord::InternalMetadata.create_table - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment end private diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 66f7d29886..d475e77444 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -17,6 +17,12 @@ module ActiveRecord # Only strings are accepted if ActiveRecord::Base.schema_format == :sql. cattr_accessor :ignore_tables, default: [] + ## + # :singleton-method: + # Specify a custom regular expression matching foreign keys which name + # should not be dumped to db/schema.rb. + cattr_accessor :fk_ignore_pattern, default: /^fk_rails_[0-9a-f]{10}$/ + class << self def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base) connection.create_schema_dumper(generate_options(config)).dump(stream) @@ -44,7 +50,7 @@ module ActiveRecord def initialize(connection, options = {}) @connection = connection - @version = Migrator::current_version rescue nil + @version = connection.migration_context.current_version rescue nil @options = options end @@ -65,11 +71,11 @@ module ActiveRecord # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. @@ -184,8 +190,9 @@ HEADER "name: #{index.name.inspect}", ] index_parts << "unique: true" if index.unique - index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present? - index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present? + index_parts << "length: #{format_index_parts(index.lengths)}" if index.lengths.present? + index_parts << "order: #{format_index_parts(index.orders)}" if index.orders.present? + index_parts << "opclass: #{format_index_parts(index.opclasses)}" if index.opclasses.present? index_parts << "where: #{index.where.inspect}" if index.where index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index) index_parts << "type: #{index.type.inspect}" if index.type @@ -209,7 +216,7 @@ HEADER parts << "primary_key: #{foreign_key.primary_key.inspect}" end - if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/ + if foreign_key.export_name_on_schema_dump? parts << "name: #{foreign_key.name.inspect}" end @@ -231,6 +238,14 @@ HEADER options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ") end + def format_index_parts(options) + if options.is_a?(Hash) + "{ #{format_options(options)} }" + else + options.inspect + end + end + def remove_prefix_and_suffix(table) prefix = Regexp.escape(@options[:table_name_prefix].to_s) suffix = Regexp.escape(@options[:table_name_suffix].to_s) diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index da585a9562..01ac56570a 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -11,23 +11,23 @@ module ActiveRecord include Named end - module ClassMethods - def current_scope(skip_inherited_scope = false) # :nodoc: + module ClassMethods # :nodoc: + def current_scope(skip_inherited_scope = false) ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) end - def current_scope=(scope) #:nodoc: + def current_scope=(scope) ScopeRegistry.set_value_for(:current_scope, self, scope) end # Collects attributes from scopes that should be applied when creating # an AR instance for the particular class this is called on. - def scope_attributes # :nodoc: + def scope_attributes all.scope_for_create end # Are there attributes associated with this scope? - def scope_attributes? # :nodoc: + def scope_attributes? current_scope end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 86ae374318..8c612df27a 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -111,7 +111,7 @@ module ActiveRecord # The user has defined their own default scope method, so call that evaluate_default_scope do if scope = default_scope - (base_rel ||= relation).merge(scope) + (base_rel ||= relation).merge!(scope) end end elsif default_scopes.any? @@ -119,7 +119,7 @@ module ActiveRecord evaluate_default_scope do default_scopes.inject(base_rel) do |default_scope, scope| scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call) - default_scope.merge(base_rel.instance_exec(&scope)) + default_scope.merge!(base_rel.instance_exec(&scope)) end end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 310af72c41..a784001587 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -171,13 +171,19 @@ module ActiveRecord "a class method with the same name." end + if method_defined_within?(name, Relation) + raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ + "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \ + "an instance method with the same name." + end + valid_scope_name?(name) extension = Module.new(&block) if block if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| scope = all - scope = scope.instance_exec(*args, &body) || scope + scope = scope._exec_scope(*args, &body) scope = scope.extending(extension) if extension scope end diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 59acd63a0f..b41d3504fd 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -114,8 +114,7 @@ module ActiveRecord end end - protected - + private attr_reader :query_builder, :bind_map, :klass end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 202b82fa61..3537e2d008 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -17,8 +17,8 @@ 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 - # the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store]. + # NOTE: If you are using structured database data types (eg. PostgreSQL +hstore+/+json+, or MySQL 5.7+ + # +json+) there is no need for the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store]. # Simply use {.store_accessor}[rdoc-ref:ClassMethods#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. @@ -31,10 +31,18 @@ module ActiveRecord # # class User < ActiveRecord::Base # store :settings, accessors: [ :color, :homepage ], coder: JSON + # store :parent, accessors: [ :name ], coder: JSON, prefix: true + # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner + # store :settings, accessors: [ :two_factor_auth ], suffix: true + # store :settings, accessors: [ :login_retry ], suffix: :config # end # - # u = User.new(color: 'black', homepage: '37signals.com') + # u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily') # u.color # Accessor stored attribute + # u.parent_name # Accessor stored attribute with prefix + # u.partner_name # Accessor stored attribute with custom prefix + # u.two_factor_auth_settings # Accessor stored attribute with suffix + # u.login_retry_config # Accessor stored attribute with custom suffix # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor # # # There is no difference between strings and symbols for accessing custom attributes @@ -44,11 +52,13 @@ module ActiveRecord # # Add additional accessors to an existing store through store_accessor # class SuperUser < User # store_accessor :settings, :privileges, :servants + # store_accessor :parent, :birthday, prefix: true + # store_accessor :settings, :secret_question, suffix: :config # end # # The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes]. # - # User.stored_attributes[:settings] # [:color, :homepage] + # User.stored_attributes[:settings] # [:color, :homepage, :two_factor_auth, :login_retry] # # == Overwriting default accessors # @@ -81,19 +91,40 @@ module ActiveRecord module ClassMethods def store(store_attribute, options = {}) serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder]) - store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors + store_accessor(store_attribute, options[:accessors], options.slice(:prefix, :suffix)) if options.has_key? :accessors end - def store_accessor(store_attribute, *keys) + def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil) keys = keys.flatten + accessor_prefix = + case prefix + when String, Symbol + "#{prefix}_" + when TrueClass + "#{store_attribute}_" + else + "" + end + accessor_suffix = + case suffix + when String, Symbol + "_#{suffix}" + when TrueClass + "_#{store_attribute}" + else + "" + end + _store_accessors_module.module_eval do keys.each do |key| - define_method("#{key}=") do |value| + accessor_key = "#{accessor_prefix}#{key}#{accessor_suffix}" + + define_method("#{accessor_key}=") do |value| write_store_attribute(store_attribute, key, value) end - define_method(key) do + define_method(accessor_key) do read_store_attribute(store_attribute, key) end end @@ -135,7 +166,7 @@ module ActiveRecord end def store_accessor_for(store_attribute) - type_for_attribute(store_attribute.to_s).accessor + type_for_attribute(store_attribute).accessor end class HashAccessor # :nodoc: diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index a5187efc84..b67479fb6a 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -2,8 +2,7 @@ module ActiveRecord class TableMetadata # :nodoc: - delegate :foreign_type, :foreign_key, :join_keys, :join_foreign_key, to: :association, prefix: true - delegate :association_primary_key, to: :association + delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true def initialize(klass, arel_table, association = nil) @klass = klass @@ -31,7 +30,7 @@ module ActiveRecord def type(column_name) if klass - klass.type_for_attribute(column_name.to_s) + klass.type_for_attribute(column_name) else Type.default_value end @@ -66,10 +65,15 @@ module ActiveRecord association && association.polymorphic? end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected + def aggregated_with?(aggregation_name) + klass && reflect_on_aggregation(aggregation_name) + end + + def reflect_on_aggregation(aggregation_name) + klass.reflect_on_aggregation(aggregation_name) + end + private 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 4657e51e6d..fd36c0abd2 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -8,7 +8,7 @@ module ActiveRecord # ActiveRecord::Tasks::DatabaseTasks is a utility class, which encapsulates # logic behind common tasks used to manage database and migrations. # - # The tasks defined here are used with Rake tasks provided by Active Record. + # The tasks defined here are used with Rails commands provided by Active Record. # # In order to use DatabaseTasks, a few config values need to be set. All the needed # config values are set by Rails already, so it's necessary to do it only if you @@ -54,10 +54,10 @@ module ActiveRecord def check_protected_environments! unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] - current = ActiveRecord::Migrator.current_environment - stored = ActiveRecord::Migrator.last_stored_environment + current = ActiveRecord::Base.connection.migration_context.current_environment + stored = ActiveRecord::Base.connection.migration_context.last_stored_environment - if ActiveRecord::Migrator.protected_environment? + if ActiveRecord::Base.connection.migration_context.protected_environment? raise ActiveRecord::ProtectedEnvironmentError.new(stored) end @@ -117,9 +117,9 @@ module ActiveRecord def create(*arguments) configuration = arguments.first class_for_adapter(configuration["adapter"]).new(*arguments).create - $stdout.puts "Created database '#{configuration['database']}'" + $stdout.puts "Created database '#{configuration['database']}'" if verbose? rescue DatabaseAlreadyExists - $stderr.puts "Database '#{configuration['database']}' already exists" + $stderr.puts "Database '#{configuration['database']}' already exists" if verbose? rescue Exception => error $stderr.puts error $stderr.puts "Couldn't create database for #{configuration.inspect}" @@ -134,6 +134,18 @@ module ActiveRecord end end + def for_each + databases = Rails.application.config.load_database_yaml + database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases) + + # if this is a single database application we don't want tasks for each primary database + return if database_configs.count == 1 + + database_configs.each do |db_config| + yield db_config.spec_name + end + end + def create_current(environment = env) each_current_configuration(environment) { |configuration| create configuration @@ -144,7 +156,7 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first class_for_adapter(configuration["adapter"]).new(*arguments).drop - $stdout.puts "Dropped database '#{configuration['database']}'" + $stdout.puts "Dropped database '#{configuration['database']}'" if verbose? rescue ActiveRecord::NoDatabaseError $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error @@ -166,10 +178,9 @@ module ActiveRecord def migrate check_target_version - verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true scope = ENV["SCOPE"] - verbose_was, Migration.verbose = Migration.verbose, verbose - Migrator.migrate(migrations_paths, target_version) do |migration| + verbose_was, Migration.verbose = Migration.verbose, verbose? + Base.connection.migration_context.migrate(target_version) do |migration| scope.blank? || scope == migration.scope end ActiveRecord::Base.clear_cache! @@ -234,9 +245,10 @@ module ActiveRecord class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename, structure_load_flags) end - def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env) # :nodoc: - file ||= schema_file(format) + def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary") # :nodoc: + file ||= dump_filename(spec_name, format) + verbose_was, Migration.verbose = Migration.verbose, verbose? && ENV["VERBOSE"] check_schema_file(file) ActiveRecord::Base.establish_connection(configuration) @@ -250,20 +262,36 @@ module ActiveRecord end ActiveRecord::InternalMetadata.create_table ActiveRecord::InternalMetadata[:environment] = environment + ensure + Migration.verbose = verbose_was end def schema_file(format = ActiveRecord::Base.schema_format) + File.join(db_dir, schema_file_type(format)) + end + + def schema_file_type(format = ActiveRecord::Base.schema_format) case format when :ruby - File.join(db_dir, "schema.rb") + "schema.rb" when :sql - File.join(db_dir, "structure.sql") + "structure.sql" end end + def dump_filename(namespace, format = ActiveRecord::Base.schema_format) + filename = if namespace == "primary" + schema_file_type(format) + else + "#{namespace}_#{schema_file_type(format)}" + end + + ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) + end + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) - each_current_configuration(environment) { |configuration, configuration_environment| - load_schema configuration, format, file, configuration_environment + each_current_configuration(environment) { |configuration, spec_name, env| + load_schema(configuration, format, file, env, spec_name) } ActiveRecord::Base.establish_connection(environment.to_sym) end @@ -297,6 +325,9 @@ module ActiveRecord end private + def verbose? + ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true + end def class_for_adapter(adapter) _key, task = @tasks.each_pair.detect { |pattern, _task| adapter[pattern] } @@ -310,10 +341,10 @@ module ActiveRecord environments = [environment] environments << "test" if environment == "development" - ActiveRecord::Base.configurations.slice(*environments).each do |configuration_environment, configuration| - next unless configuration["database"] - - yield configuration, configuration_environment + environments.each do |env| + ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration| + yield configuration, spec_name, env + end end end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index e697fa6def..eddc6fa223 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -68,9 +68,7 @@ module ActiveRecord private - def configuration - @configuration - end + attr_reader :configuration def configuration_without_database configuration.merge("database" => nil) diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 647e066137..533e6953a4 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -90,9 +90,7 @@ module ActiveRecord private - def configuration - @configuration - end + attr_reader :configuration def encoding configuration["encoding"] || DEFAULT_ENCODING diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index dfe599c4dd..d0bad05176 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -60,13 +60,7 @@ module ActiveRecord private - def configuration - @configuration - end - - def root - @root - end + attr_reader :configuration, :root def run_cmd(cmd, args, out) fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out) diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb new file mode 100644 index 0000000000..5b4efe22c9 --- /dev/null +++ b/activerecord/lib/active_record/test_databases.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "active_support/testing/parallelization" + +module ActiveRecord + module TestDatabases # :nodoc: + ActiveSupport::Testing::Parallelization.after_fork_hook do |i| + create_and_load_schema(i, spec_name: Rails.env) + end + + ActiveSupport::Testing::Parallelization.run_cleanup_hook do |_| + drop(spec_name: Rails.env) + end + + def self.create_and_load_schema(i, spec_name:) + old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" + + connection_spec = ActiveRecord::Base.configurations[spec_name] + + connection_spec["database"] += "-#{i}" + ActiveRecord::Tasks::DatabaseTasks.create(connection_spec) + ActiveRecord::Tasks::DatabaseTasks.load_schema(connection_spec) + ensure + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env]) + ENV["VERBOSE"] = old + end + + def self.drop(spec_name:) + old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" + connection_spec = ActiveRecord::Base.configurations[spec_name] + + ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec) + ensure + ENV["VERBOSE"] = old + end + end +end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 5da3759e5a..d32f971ad1 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -52,7 +52,13 @@ module ActiveRecord clear_timestamp_attributes end - class_methods do + module ClassMethods # :nodoc: + def touch_attributes_with_time(*names, time: nil) + attribute_names = timestamp_attributes_for_update_in_model + attribute_names |= names.map(&:to_s) + attribute_names.index_with(time ||= current_time_from_proper_timezone) + end + private def timestamp_attributes_for_create_in_model timestamp_attributes_for_create.select { |c| column_names.include?(c) } diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 97cba5d1c7..c5d5fca672 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -306,9 +306,7 @@ module ActiveRecord end def save(*) #:nodoc: - rollback_active_record_state! do - with_transaction_returning_status { super } - end + with_transaction_returning_status { super } end def save!(*) #:nodoc: @@ -319,17 +317,6 @@ module ActiveRecord with_transaction_returning_status { super } end - # Reset id and @new_record if the transaction rolls back. - def rollback_active_record_state! - remember_transaction_record_state - yield - rescue Exception - restore_transaction_record_state - raise - ensure - clear_transaction_record_state - end - def before_committed! # :nodoc: _run_before_commit_without_transaction_enrollment_callbacks _run_before_commit_callbacks @@ -340,11 +327,13 @@ 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!(should_run_callbacks: true) #:nodoc: - if should_run_callbacks && destroyed? || persisted? + if should_run_callbacks && (destroyed? || persisted?) + @_committed_already_called = true _run_commit_without_transaction_enrollment_callbacks _run_commit_callbacks end ensure + @_committed_already_called = false force_clear_transaction_record_state end @@ -382,13 +371,7 @@ module ActiveRecord status = nil self.class.transaction do add_to_transaction - begin - status = yield - rescue ActiveRecord::Rollback - clear_transaction_record_state - status = nil - end - + status = yield raise ActiveRecord::Rollback unless status end status @@ -399,16 +382,26 @@ module ActiveRecord end private + attr_reader :_committed_already_called, :_trigger_update_callback, :_trigger_destroy_callback # 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 - @_start_transaction_state[:id] = id @_start_transaction_state.reverse_merge!( + id: id, new_record: @new_record, destroyed: @destroyed, frozen?: frozen?, ) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 + remember_new_record_before_last_commit + end + + def remember_new_record_before_last_commit + if _committed_already_called + @_new_record_before_last_commit = false + else + @_new_record_before_last_commit = @_start_transaction_state[:new_record] + end end # Clear the new record state and id of a record. @@ -440,22 +433,16 @@ module ActiveRecord end end - # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. - def transaction_record_state(state) - @_start_transaction_state[state] - end - # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. def transaction_include_any_action?(actions) actions.any? do |action| case action when :create - transaction_record_state(:new_record) - when :destroy - defined?(@_trigger_destroy_callback) && @_trigger_destroy_callback + persisted? && @_new_record_before_last_commit when :update - !(transaction_record_state(:new_record) || destroyed?) && - (defined?(@_trigger_update_callback) && @_trigger_update_callback) + !(@_new_record_before_last_commit || destroyed?) && _trigger_update_callback + when :destroy + _trigger_destroy_callback end end end @@ -491,7 +478,8 @@ module ActiveRecord def update_attributes_from_transaction_state(transaction_state) if transaction_state && transaction_state.finalized? - restore_transaction_record_state if transaction_state.rolledback? + restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback? + force_clear_transaction_record_state if transaction_state.fully_committed? clear_transaction_record_state if transaction_state.fully_completed? end end diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb index 3cf70eafb8..82661a328a 100644 --- a/activerecord/lib/active_record/translation.rb +++ b/activerecord/lib/active_record/translation.rb @@ -10,7 +10,7 @@ module ActiveRecord classes = [klass] return classes if klass == ActiveRecord::Base - while klass != klass.base_class + while !klass.base_class? classes << klass = klass.superclass end classes diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb index e7468aa542..b300fdfa05 100644 --- a/activerecord/lib/active_record/type/adapter_specific_registry.rb +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -52,8 +52,6 @@ module ActiveRecord priority <=> other.priority end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :name, :block, :adapter, :override @@ -114,13 +112,8 @@ module ActiveRecord super | 4 end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :options, :klass - private + attr_reader :options, :klass def matches_options?(**kwargs) options.all? do |key, value| diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index e882784691..0a2f6cb9fb 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -51,6 +51,10 @@ module ActiveRecord end end + def force_equality?(value) + coder.respond_to?(:object_class) && value.is_a?(coder.object_class) + end + private def default_value?(value) diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index af4e4e37e2..7cf8181d8e 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -14,15 +14,10 @@ module ActiveRecord connection.type_cast_from_column(column, value) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :table_name delegate :connection, to: :@klass - private - def column_for(attribute_name) if connection.schema_cache.data_source_exists?(table_name) connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index d51350ba83..663cdadb03 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -9,14 +9,11 @@ module ActiveRecord 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 = types.type_for_attribute(attr_name) type.serialize(value) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :types end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 4c2c5dd852..5a1dbc8e53 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -23,7 +23,7 @@ module ActiveRecord relation = build_relation(finder_class, attribute, value) if record.persisted? if finder_class.primary_key - relation = relation.where.not(finder_class.primary_key => record.id_in_database || record.id) + relation = relation.where.not(finder_class.primary_key => record.id_in_database) else raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") end diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb new file mode 100644 index 0000000000..7d04e1cac6 --- /dev/null +++ b/activerecord/lib/arel.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "arel/errors" + +require "arel/crud" +require "arel/factory_methods" + +require "arel/expressions" +require "arel/predications" +require "arel/window_predications" +require "arel/math" +require "arel/alias_predication" +require "arel/order_predications" +require "arel/table" +require "arel/attributes" +require "arel/compatibility/wheres" + +require "arel/visitors" +require "arel/collectors/sql_string" + +require "arel/tree_manager" +require "arel/insert_manager" +require "arel/select_manager" +require "arel/update_manager" +require "arel/delete_manager" +require "arel/nodes" + +module Arel # :nodoc: all + VERSION = "10.0.0" + + def self.sql(raw_sql) + Arel::Nodes::SqlLiteral.new raw_sql + end + + def self.star + sql "*" + end + ## Convenience Alias + Node = Arel::Nodes::Node +end diff --git a/activerecord/lib/arel/alias_predication.rb b/activerecord/lib/arel/alias_predication.rb new file mode 100644 index 0000000000..4abbbb7ef6 --- /dev/null +++ b/activerecord/lib/arel/alias_predication.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module AliasPredication + def as(other) + Nodes::As.new self, Nodes::SqlLiteral.new(other) + end + end +end diff --git a/activerecord/lib/arel/attributes.rb b/activerecord/lib/arel/attributes.rb new file mode 100644 index 0000000000..35d586c948 --- /dev/null +++ b/activerecord/lib/arel/attributes.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "arel/attributes/attribute" + +module Arel # :nodoc: all + module Attributes + ### + # Factory method to wrap a raw database +column+ to an Arel Attribute. + def self.for(column) + case column.type + when :string, :text, :binary then String + when :integer then Integer + when :float then Float + when :decimal then Decimal + when :date, :datetime, :timestamp, :time then Time + when :boolean then Boolean + else + Undefined + end + end + end +end diff --git a/activerecord/lib/arel/attributes/attribute.rb b/activerecord/lib/arel/attributes/attribute.rb new file mode 100644 index 0000000000..ecf499a23e --- /dev/null +++ b/activerecord/lib/arel/attributes/attribute.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Attributes + class Attribute < Struct.new :relation, :name + include Arel::Expressions + include Arel::Predications + include Arel::AliasPredication + include Arel::OrderPredications + include Arel::Math + + ### + # Create a node for lowering this attribute + def lower + relation.lower self + end + + def type_cast_for_database(value) + relation.type_cast_for_database(name, value) + end + + def able_to_type_cast? + relation.able_to_type_cast? + end + end + + class String < Attribute; end + class Time < Attribute; end + class Boolean < Attribute; end + class Decimal < Attribute; end + class Float < Attribute; end + class Integer < Attribute; end + class Undefined < Attribute; end + end + + Attribute = Attributes::Attribute +end diff --git a/activerecord/lib/arel/collectors/bind.rb b/activerecord/lib/arel/collectors/bind.rb new file mode 100644 index 0000000000..6f8912575d --- /dev/null +++ b/activerecord/lib/arel/collectors/bind.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Collectors + class Bind + def initialize + @binds = [] + end + + def <<(str) + self + end + + def add_bind(bind) + @binds << bind + self + end + + def value + @binds + end + end + end +end diff --git a/activerecord/lib/arel/collectors/composite.rb b/activerecord/lib/arel/collectors/composite.rb new file mode 100644 index 0000000000..d040d8598d --- /dev/null +++ b/activerecord/lib/arel/collectors/composite.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Collectors + class Composite + def initialize(left, right) + @left = left + @right = right + end + + def <<(str) + left << str + right << str + self + end + + def add_bind(bind, &block) + left.add_bind bind, &block + right.add_bind bind, &block + self + end + + def value + [left.value, right.value] + end + + protected + + attr_reader :left, :right + end + end +end diff --git a/activerecord/lib/arel/collectors/plain_string.rb b/activerecord/lib/arel/collectors/plain_string.rb new file mode 100644 index 0000000000..687d7fbf2f --- /dev/null +++ b/activerecord/lib/arel/collectors/plain_string.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Collectors + class PlainString + def initialize + @str = "".dup + end + + def value + @str + end + + def <<(str) + @str << str + self + end + end + end +end diff --git a/activerecord/lib/arel/collectors/sql_string.rb b/activerecord/lib/arel/collectors/sql_string.rb new file mode 100644 index 0000000000..c293a89a74 --- /dev/null +++ b/activerecord/lib/arel/collectors/sql_string.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "arel/collectors/plain_string" + +module Arel # :nodoc: all + module Collectors + class SQLString < PlainString + def initialize(*) + super + @bind_index = 1 + end + + def add_bind(bind) + self << yield(@bind_index) + @bind_index += 1 + self + end + + def compile(bvs) + value + end + end + end +end diff --git a/activerecord/lib/arel/collectors/substitute_binds.rb b/activerecord/lib/arel/collectors/substitute_binds.rb new file mode 100644 index 0000000000..3f40eec8a8 --- /dev/null +++ b/activerecord/lib/arel/collectors/substitute_binds.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Collectors + class SubstituteBinds + def initialize(quoter, delegate_collector) + @quoter = quoter + @delegate = delegate_collector + end + + def <<(str) + delegate << str + self + end + + def add_bind(bind) + self << quoter.quote(bind) + end + + def value + delegate.value + end + + protected + + attr_reader :quoter, :delegate + end + end +end diff --git a/activerecord/lib/arel/compatibility/wheres.rb b/activerecord/lib/arel/compatibility/wheres.rb new file mode 100644 index 0000000000..c8a73f0dae --- /dev/null +++ b/activerecord/lib/arel/compatibility/wheres.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Compatibility # :nodoc: + class Wheres # :nodoc: + include Enumerable + + module Value # :nodoc: + attr_accessor :visitor + def value + visitor.accept self + end + + def name + super.to_sym + end + end + + def initialize(engine, collection) + @engine = engine + @collection = collection + end + + def each + to_sql = Visitors::ToSql.new @engine + + @collection.each { |c| + c.extend(Value) + c.visitor = to_sql + yield c + } + end + end + end +end diff --git a/activerecord/lib/arel/crud.rb b/activerecord/lib/arel/crud.rb new file mode 100644 index 0000000000..e8a563ca4a --- /dev/null +++ b/activerecord/lib/arel/crud.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + ### + # FIXME hopefully we can remove this + module Crud + def compile_update(values, pk) + um = UpdateManager.new + + if Nodes::SqlLiteral === values + relation = @ctx.from + else + relation = values.first.first.relation + end + um.key = pk + um.table relation + um.set values + um.take @ast.limit.expr if @ast.limit + um.order(*@ast.orders) + um.wheres = @ctx.wheres + um + end + + def compile_insert(values) + im = create_insert + im.insert values + im + end + + def create_insert + InsertManager.new + end + + def compile_delete + dm = DeleteManager.new + dm.take @ast.limit.expr if @ast.limit + dm.wheres = @ctx.wheres + dm.from @ctx.froms + dm + end + end +end diff --git a/activerecord/lib/arel/delete_manager.rb b/activerecord/lib/arel/delete_manager.rb new file mode 100644 index 0000000000..2def581009 --- /dev/null +++ b/activerecord/lib/arel/delete_manager.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class DeleteManager < Arel::TreeManager + def initialize + super + @ast = Nodes::DeleteStatement.new + @ctx = @ast + end + + def from(relation) + @ast.relation = relation + self + end + + def take(limit) + @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit + self + end + + def wheres=(list) + @ast.wheres = list + end + end +end diff --git a/activerecord/lib/arel/errors.rb b/activerecord/lib/arel/errors.rb new file mode 100644 index 0000000000..2f8d5e3c02 --- /dev/null +++ b/activerecord/lib/arel/errors.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class ArelError < StandardError + end + + class EmptyJoinError < ArelError + end +end diff --git a/activerecord/lib/arel/expressions.rb b/activerecord/lib/arel/expressions.rb new file mode 100644 index 0000000000..da8afb338c --- /dev/null +++ b/activerecord/lib/arel/expressions.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Expressions + def count(distinct = false) + Nodes::Count.new [self], distinct + end + + def sum + Nodes::Sum.new [self] + end + + def maximum + Nodes::Max.new [self] + end + + def minimum + Nodes::Min.new [self] + end + + def average + Nodes::Avg.new [self] + end + + def extract(field) + Nodes::Extract.new [self], field + end + end +end diff --git a/activerecord/lib/arel/factory_methods.rb b/activerecord/lib/arel/factory_methods.rb new file mode 100644 index 0000000000..b828bc274e --- /dev/null +++ b/activerecord/lib/arel/factory_methods.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + ### + # Methods for creating various nodes + module FactoryMethods + def create_true + Arel::Nodes::True.new + end + + def create_false + Arel::Nodes::False.new + end + + def create_table_alias(relation, name) + Nodes::TableAlias.new(relation, name) + end + + def create_join(to, constraint = nil, klass = Nodes::InnerJoin) + klass.new(to, constraint) + end + + def create_string_join(to) + create_join to, nil, Nodes::StringJoin + end + + def create_and(clauses) + Nodes::And.new clauses + end + + def create_on(expr) + Nodes::On.new expr + end + + def grouping(expr) + Nodes::Grouping.new expr + end + + ### + # Create a LOWER() function + def lower(column) + Nodes::NamedFunction.new "LOWER", [Nodes.build_quoted(column)] + end + end +end diff --git a/activerecord/lib/arel/insert_manager.rb b/activerecord/lib/arel/insert_manager.rb new file mode 100644 index 0000000000..c90fc33a48 --- /dev/null +++ b/activerecord/lib/arel/insert_manager.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class InsertManager < Arel::TreeManager + def initialize + super + @ast = Nodes::InsertStatement.new + end + + def into(table) + @ast.relation = table + self + end + + def columns; @ast.columns end + def values=(val); @ast.values = val; end + + def select(select) + @ast.select = select + end + + def insert(fields) + return if fields.empty? + + if String === fields + @ast.values = Nodes::SqlLiteral.new(fields) + else + @ast.relation ||= fields.first.first.relation + + values = [] + + fields.each do |column, value| + @ast.columns << column + values << value + end + @ast.values = create_values values, @ast.columns + end + self + end + + def create_values(values, columns) + Nodes::Values.new values, columns + end + + def create_values_list(rows) + Nodes::ValuesList.new(rows) + end + end +end diff --git a/activerecord/lib/arel/math.rb b/activerecord/lib/arel/math.rb new file mode 100644 index 0000000000..2359f13148 --- /dev/null +++ b/activerecord/lib/arel/math.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Math + def *(other) + Arel::Nodes::Multiplication.new(self, other) + end + + def +(other) + Arel::Nodes::Grouping.new(Arel::Nodes::Addition.new(self, other)) + end + + def -(other) + Arel::Nodes::Grouping.new(Arel::Nodes::Subtraction.new(self, other)) + end + + def /(other) + Arel::Nodes::Division.new(self, other) + end + + def &(other) + Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(self, other)) + end + + def |(other) + Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(self, other)) + end + + def ^(other) + Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseXor.new(self, other)) + end + + def <<(other) + Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftLeft.new(self, other)) + end + + def >>(other) + Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftRight.new(self, other)) + end + + def ~@ + Arel::Nodes::BitwiseNot.new(self) + end + end +end diff --git a/activerecord/lib/arel/nodes.rb b/activerecord/lib/arel/nodes.rb new file mode 100644 index 0000000000..5af0e532e2 --- /dev/null +++ b/activerecord/lib/arel/nodes.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# node +require "arel/nodes/node" +require "arel/nodes/node_expression" +require "arel/nodes/select_statement" +require "arel/nodes/select_core" +require "arel/nodes/insert_statement" +require "arel/nodes/update_statement" +require "arel/nodes/bind_param" + +# terminal + +require "arel/nodes/terminal" +require "arel/nodes/true" +require "arel/nodes/false" + +# unary +require "arel/nodes/unary" +require "arel/nodes/grouping" +require "arel/nodes/ascending" +require "arel/nodes/descending" +require "arel/nodes/unqualified_column" +require "arel/nodes/with" + +# binary +require "arel/nodes/binary" +require "arel/nodes/equality" +require "arel/nodes/in" # Why is this subclassed from equality? +require "arel/nodes/join_source" +require "arel/nodes/delete_statement" +require "arel/nodes/table_alias" +require "arel/nodes/infix_operation" +require "arel/nodes/unary_operation" +require "arel/nodes/over" +require "arel/nodes/matches" +require "arel/nodes/regexp" + +# nary +require "arel/nodes/and" + +# function +# FIXME: Function + Alias can be rewritten as a Function and Alias node. +# We should make Function a Unary node and deprecate the use of "aliaz" +require "arel/nodes/function" +require "arel/nodes/count" +require "arel/nodes/extract" +require "arel/nodes/values" +require "arel/nodes/values_list" +require "arel/nodes/named_function" + +# windows +require "arel/nodes/window" + +# conditional expressions +require "arel/nodes/case" + +# joins +require "arel/nodes/full_outer_join" +require "arel/nodes/inner_join" +require "arel/nodes/outer_join" +require "arel/nodes/right_outer_join" +require "arel/nodes/string_join" + +require "arel/nodes/sql_literal" + +require "arel/nodes/casted" diff --git a/activerecord/lib/arel/nodes/and.rb b/activerecord/lib/arel/nodes/and.rb new file mode 100644 index 0000000000..c530a77bfb --- /dev/null +++ b/activerecord/lib/arel/nodes/and.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class And < Arel::Nodes::Node + attr_reader :children + + def initialize(children) + super() + @children = children + end + + def left + children.first + end + + def right + children[1] + end + + def hash + children.hash + end + + def eql?(other) + self.class == other.class && + self.children == other.children + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/ascending.rb b/activerecord/lib/arel/nodes/ascending.rb new file mode 100644 index 0000000000..8b617f4df5 --- /dev/null +++ b/activerecord/lib/arel/nodes/ascending.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Ascending < Ordering + def reverse + Descending.new(expr) + end + + def direction + :asc + end + + def ascending? + true + end + + def descending? + false + end + end + end +end diff --git a/activerecord/lib/arel/nodes/binary.rb b/activerecord/lib/arel/nodes/binary.rb new file mode 100644 index 0000000000..e184e99c73 --- /dev/null +++ b/activerecord/lib/arel/nodes/binary.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Binary < Arel::Nodes::NodeExpression + attr_accessor :left, :right + + def initialize(left, right) + super() + @left = left + @right = right + end + + def initialize_copy(other) + super + @left = @left.clone if @left + @right = @right.clone if @right + end + + def hash + [self.class, @left, @right].hash + end + + def eql?(other) + self.class == other.class && + self.left == other.left && + self.right == other.right + end + alias :== :eql? + end + + %w{ + As + Assignment + Between + GreaterThan + GreaterThanOrEqual + Join + LessThan + LessThanOrEqual + NotEqual + NotIn + Or + Union + UnionAll + Intersect + Except + }.each do |name| + const_set name, Class.new(Binary) + end + end +end diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb new file mode 100644 index 0000000000..53c5563d93 --- /dev/null +++ b/activerecord/lib/arel/nodes/bind_param.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class BindParam < Node + attr_accessor :value + + def initialize(value) + @value = value + super() + end + + def hash + [self.class, self.value].hash + end + + def eql?(other) + other.is_a?(BindParam) && + value == other.value + end + alias :== :eql? + + def nil? + value.nil? + end + end + end +end diff --git a/activerecord/lib/arel/nodes/case.rb b/activerecord/lib/arel/nodes/case.rb new file mode 100644 index 0000000000..654a54825e --- /dev/null +++ b/activerecord/lib/arel/nodes/case.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Case < Arel::Nodes::Node + attr_accessor :case, :conditions, :default + + def initialize(expression = nil, default = nil) + @case = expression + @conditions = [] + @default = default + end + + def when(condition, expression = nil) + @conditions << When.new(Nodes.build_quoted(condition), expression) + self + end + + def then(expression) + @conditions.last.right = Nodes.build_quoted(expression) + self + end + + def else(expression) + @default = Else.new Nodes.build_quoted(expression) + self + end + + def initialize_copy(other) + super + @case = @case.clone if @case + @conditions = @conditions.map { |x| x.clone } + @default = @default.clone if @default + end + + def hash + [@case, @conditions, @default].hash + end + + def eql?(other) + self.class == other.class && + self.case == other.case && + self.conditions == other.conditions && + self.default == other.default + end + alias :== :eql? + end + + class When < Binary # :nodoc: + end + + class Else < Unary # :nodoc: + end + end +end diff --git a/activerecord/lib/arel/nodes/casted.rb b/activerecord/lib/arel/nodes/casted.rb new file mode 100644 index 0000000000..c1e6e97d6d --- /dev/null +++ b/activerecord/lib/arel/nodes/casted.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Casted < Arel::Nodes::NodeExpression # :nodoc: + attr_reader :val, :attribute + def initialize(val, attribute) + @val = val + @attribute = attribute + super() + end + + def nil?; @val.nil?; end + + def hash + [self.class, val, attribute].hash + end + + def eql?(other) + self.class == other.class && + self.val == other.val && + self.attribute == other.attribute + end + alias :== :eql? + end + + class Quoted < Arel::Nodes::Unary # :nodoc: + alias :val :value + def nil?; val.nil?; end + end + + def self.build_quoted(other, attribute = nil) + case other + when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::Nodes::BindParam, Arel::SelectManager, Arel::Nodes::Quoted, Arel::Nodes::SqlLiteral + other + else + case attribute + when Arel::Attributes::Attribute + Casted.new other, attribute + else + Quoted.new other + end + end + end + end +end diff --git a/activerecord/lib/arel/nodes/count.rb b/activerecord/lib/arel/nodes/count.rb new file mode 100644 index 0000000000..880464639d --- /dev/null +++ b/activerecord/lib/arel/nodes/count.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Count < Arel::Nodes::Function + def initialize(expr, distinct = false, aliaz = nil) + super(expr, aliaz) + @distinct = distinct + end + end + end +end diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb new file mode 100644 index 0000000000..eaac05e2f6 --- /dev/null +++ b/activerecord/lib/arel/nodes/delete_statement.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class DeleteStatement < Arel::Nodes::Node + attr_accessor :left, :right + attr_accessor :limit + + alias :relation :left + alias :relation= :left= + alias :wheres :right + alias :wheres= :right= + + def initialize(relation = nil, wheres = []) + super() + @left = relation + @right = wheres + end + + def initialize_copy(other) + super + @left = @left.clone if @left + @right = @right.clone if @right + end + + def hash + [self.class, @left, @right].hash + end + + def eql?(other) + self.class == other.class && + self.left == other.left && + self.right == other.right + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/descending.rb b/activerecord/lib/arel/nodes/descending.rb new file mode 100644 index 0000000000..f3f6992ca8 --- /dev/null +++ b/activerecord/lib/arel/nodes/descending.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Descending < Ordering + def reverse + Ascending.new(expr) + end + + def direction + :desc + end + + def ascending? + false + end + + def descending? + true + end + end + end +end diff --git a/activerecord/lib/arel/nodes/equality.rb b/activerecord/lib/arel/nodes/equality.rb new file mode 100644 index 0000000000..2aa85a977e --- /dev/null +++ b/activerecord/lib/arel/nodes/equality.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Equality < Arel::Nodes::Binary + def operator; :== end + alias :operand1 :left + alias :operand2 :right + end + end +end diff --git a/activerecord/lib/arel/nodes/extract.rb b/activerecord/lib/arel/nodes/extract.rb new file mode 100644 index 0000000000..5799ee9b8f --- /dev/null +++ b/activerecord/lib/arel/nodes/extract.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Extract < Arel::Nodes::Unary + attr_accessor :field + + def initialize(expr, field) + super(expr) + @field = field + end + + def hash + super ^ @field.hash + end + + def eql?(other) + super && + self.field == other.field + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/false.rb b/activerecord/lib/arel/nodes/false.rb new file mode 100644 index 0000000000..1e5bf04be5 --- /dev/null +++ b/activerecord/lib/arel/nodes/false.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class False < Arel::Nodes::NodeExpression + def hash + self.class.hash + end + + def eql?(other) + self.class == other.class + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/full_outer_join.rb b/activerecord/lib/arel/nodes/full_outer_join.rb new file mode 100644 index 0000000000..91bb81f2e3 --- /dev/null +++ b/activerecord/lib/arel/nodes/full_outer_join.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class FullOuterJoin < Arel::Nodes::Join + end + end +end diff --git a/activerecord/lib/arel/nodes/function.rb b/activerecord/lib/arel/nodes/function.rb new file mode 100644 index 0000000000..0a439b39f5 --- /dev/null +++ b/activerecord/lib/arel/nodes/function.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Function < Arel::Nodes::NodeExpression + include Arel::WindowPredications + attr_accessor :expressions, :alias, :distinct + + def initialize(expr, aliaz = nil) + super() + @expressions = expr + @alias = aliaz && SqlLiteral.new(aliaz) + @distinct = false + end + + def as(aliaz) + self.alias = SqlLiteral.new(aliaz) + self + end + + def hash + [@expressions, @alias, @distinct].hash + end + + def eql?(other) + self.class == other.class && + self.expressions == other.expressions && + self.alias == other.alias && + self.distinct == other.distinct + end + alias :== :eql? + end + + %w{ + Sum + Exists + Max + Min + Avg + }.each do |name| + const_set(name, Class.new(Function)) + end + end +end diff --git a/activerecord/lib/arel/nodes/grouping.rb b/activerecord/lib/arel/nodes/grouping.rb new file mode 100644 index 0000000000..4d0bd69d4d --- /dev/null +++ b/activerecord/lib/arel/nodes/grouping.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Grouping < Unary + end + end +end diff --git a/activerecord/lib/arel/nodes/in.rb b/activerecord/lib/arel/nodes/in.rb new file mode 100644 index 0000000000..2be45d6f99 --- /dev/null +++ b/activerecord/lib/arel/nodes/in.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class In < Equality + end + end +end diff --git a/activerecord/lib/arel/nodes/infix_operation.rb b/activerecord/lib/arel/nodes/infix_operation.rb new file mode 100644 index 0000000000..bc7e20dcc6 --- /dev/null +++ b/activerecord/lib/arel/nodes/infix_operation.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class InfixOperation < Binary + include Arel::Expressions + include Arel::Predications + include Arel::OrderPredications + include Arel::AliasPredication + include Arel::Math + + attr_reader :operator + + def initialize(operator, left, right) + super(left, right) + @operator = operator + end + end + + class Multiplication < InfixOperation + def initialize(left, right) + super(:*, left, right) + end + end + + class Division < InfixOperation + def initialize(left, right) + super(:/, left, right) + end + end + + class Addition < InfixOperation + def initialize(left, right) + super(:+, left, right) + end + end + + class Subtraction < InfixOperation + def initialize(left, right) + super(:-, left, right) + end + end + + class Concat < InfixOperation + def initialize(left, right) + super("||", left, right) + end + end + + class BitwiseAnd < InfixOperation + def initialize(left, right) + super(:&, left, right) + end + end + + class BitwiseOr < InfixOperation + def initialize(left, right) + super(:|, left, right) + end + end + + class BitwiseXor < InfixOperation + def initialize(left, right) + super(:^, left, right) + end + end + + class BitwiseShiftLeft < InfixOperation + def initialize(left, right) + super(:<<, left, right) + end + end + + class BitwiseShiftRight < InfixOperation + def initialize(left, right) + super(:>>, left, right) + end + end + end +end diff --git a/activerecord/lib/arel/nodes/inner_join.rb b/activerecord/lib/arel/nodes/inner_join.rb new file mode 100644 index 0000000000..519fafad09 --- /dev/null +++ b/activerecord/lib/arel/nodes/inner_join.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class InnerJoin < Arel::Nodes::Join + end + end +end diff --git a/activerecord/lib/arel/nodes/insert_statement.rb b/activerecord/lib/arel/nodes/insert_statement.rb new file mode 100644 index 0000000000..d28fd1f6c8 --- /dev/null +++ b/activerecord/lib/arel/nodes/insert_statement.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class InsertStatement < Arel::Nodes::Node + attr_accessor :relation, :columns, :values, :select + + def initialize + super() + @relation = nil + @columns = [] + @values = nil + @select = nil + end + + def initialize_copy(other) + super + @columns = @columns.clone + @values = @values.clone if @values + @select = @select.clone if @select + end + + def hash + [@relation, @columns, @values, @select].hash + end + + def eql?(other) + self.class == other.class && + self.relation == other.relation && + self.columns == other.columns && + self.select == other.select && + self.values == other.values + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/join_source.rb b/activerecord/lib/arel/nodes/join_source.rb new file mode 100644 index 0000000000..abf0944623 --- /dev/null +++ b/activerecord/lib/arel/nodes/join_source.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + ### + # Class that represents a join source + # + # http://www.sqlite.org/syntaxdiagrams.html#join-source + + class JoinSource < Arel::Nodes::Binary + def initialize(single_source, joinop = []) + super + end + + def empty? + !left && right.empty? + end + end + end +end diff --git a/activerecord/lib/arel/nodes/matches.rb b/activerecord/lib/arel/nodes/matches.rb new file mode 100644 index 0000000000..fd5734f4bd --- /dev/null +++ b/activerecord/lib/arel/nodes/matches.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Matches < Binary + attr_reader :escape + attr_accessor :case_sensitive + + def initialize(left, right, escape = nil, case_sensitive = false) + super(left, right) + @escape = escape && Nodes.build_quoted(escape) + @case_sensitive = case_sensitive + end + end + + class DoesNotMatch < Matches; end + end +end diff --git a/activerecord/lib/arel/nodes/named_function.rb b/activerecord/lib/arel/nodes/named_function.rb new file mode 100644 index 0000000000..126462d6d6 --- /dev/null +++ b/activerecord/lib/arel/nodes/named_function.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class NamedFunction < Arel::Nodes::Function + attr_accessor :name + + def initialize(name, expr, aliaz = nil) + super(expr, aliaz) + @name = name + end + + def hash + super ^ @name.hash + end + + def eql?(other) + super && self.name == other.name + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb new file mode 100644 index 0000000000..2b9b1e9828 --- /dev/null +++ b/activerecord/lib/arel/nodes/node.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + ### + # Abstract base class for all AST nodes + class Node + include Arel::FactoryMethods + include Enumerable + + if $DEBUG + def _caller + @caller + end + + def initialize + @caller = caller.dup + end + end + + ### + # Factory method to create a Nodes::Not node that has the recipient of + # the caller as a child. + def not + Nodes::Not.new self + end + + ### + # Factory method to create a Nodes::Grouping node that has an Nodes::Or + # node as a child. + def or(right) + Nodes::Grouping.new Nodes::Or.new(self, right) + end + + ### + # Factory method to create an Nodes::And node. + def and(right) + Nodes::And.new [self, right] + end + + # FIXME: this method should go away. I don't like people calling + # to_sql on non-head nodes. This forces us to walk the AST until we + # can find a node that has a "relation" member. + # + # Maybe we should just use `Table.engine`? :'( + def to_sql(engine = Table.engine) + collector = Arel::Collectors::SQLString.new + collector = engine.connection.visitor.accept self, collector + collector.value + end + + # Iterate through AST, nodes will be yielded depth-first + def each(&block) + return enum_for(:each) unless block_given? + + ::Arel::Visitors::DepthFirst.new(block).accept self + end + end + end +end diff --git a/activerecord/lib/arel/nodes/node_expression.rb b/activerecord/lib/arel/nodes/node_expression.rb new file mode 100644 index 0000000000..cbcfaba37c --- /dev/null +++ b/activerecord/lib/arel/nodes/node_expression.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class NodeExpression < Arel::Nodes::Node + include Arel::Expressions + include Arel::Predications + include Arel::AliasPredication + include Arel::OrderPredications + include Arel::Math + end + end +end diff --git a/activerecord/lib/arel/nodes/outer_join.rb b/activerecord/lib/arel/nodes/outer_join.rb new file mode 100644 index 0000000000..0a3042be61 --- /dev/null +++ b/activerecord/lib/arel/nodes/outer_join.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class OuterJoin < Arel::Nodes::Join + end + end +end diff --git a/activerecord/lib/arel/nodes/over.rb b/activerecord/lib/arel/nodes/over.rb new file mode 100644 index 0000000000..91176764a9 --- /dev/null +++ b/activerecord/lib/arel/nodes/over.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Over < Binary + include Arel::AliasPredication + + def initialize(left, right = nil) + super(left, right) + end + + def operator; "OVER" end + end + end +end diff --git a/activerecord/lib/arel/nodes/regexp.rb b/activerecord/lib/arel/nodes/regexp.rb new file mode 100644 index 0000000000..7c25095569 --- /dev/null +++ b/activerecord/lib/arel/nodes/regexp.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Regexp < Binary + attr_accessor :case_sensitive + + def initialize(left, right, case_sensitive = true) + super(left, right) + @case_sensitive = case_sensitive + end + end + + class NotRegexp < Regexp; end + end +end diff --git a/activerecord/lib/arel/nodes/right_outer_join.rb b/activerecord/lib/arel/nodes/right_outer_join.rb new file mode 100644 index 0000000000..04ed4aaa78 --- /dev/null +++ b/activerecord/lib/arel/nodes/right_outer_join.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class RightOuterJoin < Arel::Nodes::Join + end + end +end diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb new file mode 100644 index 0000000000..2defe61974 --- /dev/null +++ b/activerecord/lib/arel/nodes/select_core.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class SelectCore < Arel::Nodes::Node + attr_accessor :top, :projections, :wheres, :groups, :windows + attr_accessor :havings, :source, :set_quantifier + + def initialize + super() + @source = JoinSource.new nil + @top = nil + + # https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier + @set_quantifier = nil + @projections = [] + @wheres = [] + @groups = [] + @havings = [] + @windows = [] + end + + def from + @source.left + end + + def from=(value) + @source.left = value + end + + alias :froms= :from= + alias :froms :from + + def initialize_copy(other) + super + @source = @source.clone if @source + @projections = @projections.clone + @wheres = @wheres.clone + @groups = @groups.clone + @havings = @havings.clone + @windows = @windows.clone + end + + def hash + [ + @source, @top, @set_quantifier, @projections, + @wheres, @groups, @havings, @windows + ].hash + end + + def eql?(other) + self.class == other.class && + self.source == other.source && + self.top == other.top && + self.set_quantifier == other.set_quantifier && + self.projections == other.projections && + self.wheres == other.wheres && + self.groups == other.groups && + self.havings == other.havings && + self.windows == other.windows + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/select_statement.rb b/activerecord/lib/arel/nodes/select_statement.rb new file mode 100644 index 0000000000..eff5dad939 --- /dev/null +++ b/activerecord/lib/arel/nodes/select_statement.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class SelectStatement < Arel::Nodes::NodeExpression + attr_reader :cores + attr_accessor :limit, :orders, :lock, :offset, :with + + def initialize(cores = [SelectCore.new]) + super() + @cores = cores + @orders = [] + @limit = nil + @lock = nil + @offset = nil + @with = nil + end + + def initialize_copy(other) + super + @cores = @cores.map { |x| x.clone } + @orders = @orders.map { |x| x.clone } + end + + def hash + [@cores, @orders, @limit, @lock, @offset, @with].hash + end + + def eql?(other) + self.class == other.class && + self.cores == other.cores && + self.orders == other.orders && + self.limit == other.limit && + self.lock == other.lock && + self.offset == other.offset && + self.with == other.with + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/sql_literal.rb b/activerecord/lib/arel/nodes/sql_literal.rb new file mode 100644 index 0000000000..d25a8521b7 --- /dev/null +++ b/activerecord/lib/arel/nodes/sql_literal.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class SqlLiteral < String + include Arel::Expressions + include Arel::Predications + include Arel::AliasPredication + include Arel::OrderPredications + + def encode_with(coder) + coder.scalar = self.to_s + end + end + end +end diff --git a/activerecord/lib/arel/nodes/string_join.rb b/activerecord/lib/arel/nodes/string_join.rb new file mode 100644 index 0000000000..86027fcab7 --- /dev/null +++ b/activerecord/lib/arel/nodes/string_join.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class StringJoin < Arel::Nodes::Join + def initialize(left, right = nil) + super + end + end + end +end diff --git a/activerecord/lib/arel/nodes/table_alias.rb b/activerecord/lib/arel/nodes/table_alias.rb new file mode 100644 index 0000000000..f95ca16a3d --- /dev/null +++ b/activerecord/lib/arel/nodes/table_alias.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class TableAlias < Arel::Nodes::Binary + alias :name :right + alias :relation :left + alias :table_alias :name + + def [](name) + Attribute.new(self, name) + end + + def table_name + relation.respond_to?(:name) ? relation.name : name + end + + def type_cast_for_database(*args) + relation.type_cast_for_database(*args) + end + + def able_to_type_cast? + relation.respond_to?(:able_to_type_cast?) && relation.able_to_type_cast? + end + end + end +end diff --git a/activerecord/lib/arel/nodes/terminal.rb b/activerecord/lib/arel/nodes/terminal.rb new file mode 100644 index 0000000000..d84c453f1a --- /dev/null +++ b/activerecord/lib/arel/nodes/terminal.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Distinct < Arel::Nodes::NodeExpression + def hash + self.class.hash + end + + def eql?(other) + self.class == other.class + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/true.rb b/activerecord/lib/arel/nodes/true.rb new file mode 100644 index 0000000000..c891012969 --- /dev/null +++ b/activerecord/lib/arel/nodes/true.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class True < Arel::Nodes::NodeExpression + def hash + self.class.hash + end + + def eql?(other) + self.class == other.class + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb new file mode 100644 index 0000000000..a3c0045897 --- /dev/null +++ b/activerecord/lib/arel/nodes/unary.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Unary < Arel::Nodes::NodeExpression + attr_accessor :expr + alias :value :expr + + def initialize(expr) + super() + @expr = expr + end + + def hash + @expr.hash + end + + def eql?(other) + self.class == other.class && + self.expr == other.expr + end + alias :== :eql? + end + + %w{ + Bin + Cube + DistinctOn + Group + GroupingElement + GroupingSet + Lateral + Limit + Lock + Not + Offset + On + Ordering + RollUp + Top + }.each do |name| + const_set(name, Class.new(Unary)) + end + end +end diff --git a/activerecord/lib/arel/nodes/unary_operation.rb b/activerecord/lib/arel/nodes/unary_operation.rb new file mode 100644 index 0000000000..524282ac84 --- /dev/null +++ b/activerecord/lib/arel/nodes/unary_operation.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class UnaryOperation < Unary + attr_reader :operator + + def initialize(operator, operand) + super(operand) + @operator = operator + end + end + + class BitwiseNot < UnaryOperation + def initialize(operand) + super(:~, operand) + end + end + end +end diff --git a/activerecord/lib/arel/nodes/unqualified_column.rb b/activerecord/lib/arel/nodes/unqualified_column.rb new file mode 100644 index 0000000000..7c3e0720d7 --- /dev/null +++ b/activerecord/lib/arel/nodes/unqualified_column.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class UnqualifiedColumn < Arel::Nodes::Unary + alias :attribute :expr + alias :attribute= :expr= + + def relation + @expr.relation + end + + def column + @expr.column + end + + def name + @expr.name + end + end + end +end diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb new file mode 100644 index 0000000000..5184b1180f --- /dev/null +++ b/activerecord/lib/arel/nodes/update_statement.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class UpdateStatement < Arel::Nodes::Node + attr_accessor :relation, :wheres, :values, :orders, :limit + attr_accessor :key + + def initialize + @relation = nil + @wheres = [] + @values = [] + @orders = [] + @limit = nil + @key = nil + end + + def initialize_copy(other) + super + @wheres = @wheres.clone + @values = @values.clone + end + + def hash + [@relation, @wheres, @values, @orders, @limit, @key].hash + end + + def eql?(other) + self.class == other.class && + self.relation == other.relation && + self.wheres == other.wheres && + self.values == other.values && + self.orders == other.orders && + self.limit == other.limit && + self.key == other.key + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/values.rb b/activerecord/lib/arel/nodes/values.rb new file mode 100644 index 0000000000..650248dc04 --- /dev/null +++ b/activerecord/lib/arel/nodes/values.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Values < Arel::Nodes::Binary + alias :expressions :left + alias :expressions= :left= + alias :columns :right + alias :columns= :right= + + def initialize(exprs, columns = []) + super + end + end + end +end diff --git a/activerecord/lib/arel/nodes/values_list.rb b/activerecord/lib/arel/nodes/values_list.rb new file mode 100644 index 0000000000..27109848e4 --- /dev/null +++ b/activerecord/lib/arel/nodes/values_list.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class ValuesList < Node + attr_reader :rows + + def initialize(rows) + @rows = rows + super() + end + + def hash + @rows.hash + end + + def eql?(other) + self.class == other.class && + self.rows == other.rows + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/window.rb b/activerecord/lib/arel/nodes/window.rb new file mode 100644 index 0000000000..4916fc7fbe --- /dev/null +++ b/activerecord/lib/arel/nodes/window.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Window < Arel::Nodes::Node + attr_accessor :orders, :framing, :partitions + + def initialize + @orders = [] + @partitions = [] + @framing = nil + end + + def order(*expr) + # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically + @orders.concat expr.map { |x| + String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x + } + self + end + + def partition(*expr) + # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically + @partitions.concat expr.map { |x| + String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x + } + self + end + + def frame(expr) + @framing = expr + end + + def rows(expr = nil) + if @framing + Rows.new(expr) + else + frame(Rows.new(expr)) + end + end + + def range(expr = nil) + if @framing + Range.new(expr) + else + frame(Range.new(expr)) + end + end + + def initialize_copy(other) + super + @orders = @orders.map { |x| x.clone } + end + + def hash + [@orders, @framing].hash + end + + def eql?(other) + self.class == other.class && + self.orders == other.orders && + self.framing == other.framing && + self.partitions == other.partitions + end + alias :== :eql? + end + + class NamedWindow < Window + attr_accessor :name + + def initialize(name) + super() + @name = name + end + + def initialize_copy(other) + super + @name = other.name.clone + end + + def hash + super ^ @name.hash + end + + def eql?(other) + super && self.name == other.name + end + alias :== :eql? + end + + class Rows < Unary + def initialize(expr = nil) + super(expr) + end + end + + class Range < Unary + def initialize(expr = nil) + super(expr) + end + end + + class CurrentRow < Node + def hash + self.class.hash + end + + def eql?(other) + self.class == other.class + end + alias :== :eql? + end + + class Preceding < Unary + def initialize(expr = nil) + super(expr) + end + end + + class Following < Unary + def initialize(expr = nil) + super(expr) + end + end + end +end diff --git a/activerecord/lib/arel/nodes/with.rb b/activerecord/lib/arel/nodes/with.rb new file mode 100644 index 0000000000..157bdcaa08 --- /dev/null +++ b/activerecord/lib/arel/nodes/with.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class With < Arel::Nodes::Unary + alias children expr + end + + class WithRecursive < With; end + end +end diff --git a/activerecord/lib/arel/order_predications.rb b/activerecord/lib/arel/order_predications.rb new file mode 100644 index 0000000000..d785bbba92 --- /dev/null +++ b/activerecord/lib/arel/order_predications.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module OrderPredications + def asc + Nodes::Ascending.new self + end + + def desc + Nodes::Descending.new self + end + end +end diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb new file mode 100644 index 0000000000..e83a6f162f --- /dev/null +++ b/activerecord/lib/arel/predications.rb @@ -0,0 +1,241 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Predications + def not_eq(other) + Nodes::NotEqual.new self, quoted_node(other) + end + + def not_eq_any(others) + grouping_any :not_eq, others + end + + def not_eq_all(others) + grouping_all :not_eq, others + end + + def eq(other) + Nodes::Equality.new self, quoted_node(other) + end + + def eq_any(others) + grouping_any :eq, others + end + + def eq_all(others) + grouping_all :eq, quoted_array(others) + end + + def between(other) + if equals_quoted?(other.begin, -Float::INFINITY) + if equals_quoted?(other.end, Float::INFINITY) + not_in([]) + elsif other.exclude_end? + lt(other.end) + else + lteq(other.end) + end + elsif equals_quoted?(other.end, Float::INFINITY) + gteq(other.begin) + elsif other.exclude_end? + gteq(other.begin).and(lt(other.end)) + else + left = quoted_node(other.begin) + right = quoted_node(other.end) + Nodes::Between.new(self, left.and(right)) + end + end + + def in(other) + case other + when Arel::SelectManager + Arel::Nodes::In.new(self, other.ast) + when Range + if $VERBOSE + warn <<-eowarn +Passing a range to `#in` is deprecated. Call `#between`, instead. + eowarn + end + between(other) + when Enumerable + Nodes::In.new self, quoted_array(other) + else + Nodes::In.new self, quoted_node(other) + end + end + + def in_any(others) + grouping_any :in, others + end + + def in_all(others) + grouping_all :in, others + end + + def not_between(other) + if equals_quoted?(other.begin, -Float::INFINITY) + if equals_quoted?(other.end, Float::INFINITY) + self.in([]) + elsif other.exclude_end? + gteq(other.end) + else + gt(other.end) + end + elsif equals_quoted?(other.end, Float::INFINITY) + lt(other.begin) + else + left = lt(other.begin) + right = if other.exclude_end? + gteq(other.end) + else + gt(other.end) + end + left.or(right) + end + end + + def not_in(other) + case other + when Arel::SelectManager + Arel::Nodes::NotIn.new(self, other.ast) + when Range + if $VERBOSE + warn <<-eowarn +Passing a range to `#not_in` is deprecated. Call `#not_between`, instead. + eowarn + end + not_between(other) + when Enumerable + Nodes::NotIn.new self, quoted_array(other) + else + Nodes::NotIn.new self, quoted_node(other) + end + end + + def not_in_any(others) + grouping_any :not_in, others + end + + def not_in_all(others) + grouping_all :not_in, others + end + + def matches(other, escape = nil, case_sensitive = false) + Nodes::Matches.new self, quoted_node(other), escape, case_sensitive + end + + def matches_regexp(other, case_sensitive = true) + Nodes::Regexp.new self, quoted_node(other), case_sensitive + end + + def matches_any(others, escape = nil, case_sensitive = false) + grouping_any :matches, others, escape, case_sensitive + end + + def matches_all(others, escape = nil, case_sensitive = false) + grouping_all :matches, others, escape, case_sensitive + end + + def does_not_match(other, escape = nil, case_sensitive = false) + Nodes::DoesNotMatch.new self, quoted_node(other), escape, case_sensitive + end + + def does_not_match_regexp(other, case_sensitive = true) + Nodes::NotRegexp.new self, quoted_node(other), case_sensitive + end + + def does_not_match_any(others, escape = nil) + grouping_any :does_not_match, others, escape + end + + def does_not_match_all(others, escape = nil) + grouping_all :does_not_match, others, escape + end + + def gteq(right) + Nodes::GreaterThanOrEqual.new self, quoted_node(right) + end + + def gteq_any(others) + grouping_any :gteq, others + end + + def gteq_all(others) + grouping_all :gteq, others + end + + def gt(right) + Nodes::GreaterThan.new self, quoted_node(right) + end + + def gt_any(others) + grouping_any :gt, others + end + + def gt_all(others) + grouping_all :gt, others + end + + def lt(right) + Nodes::LessThan.new self, quoted_node(right) + end + + def lt_any(others) + grouping_any :lt, others + end + + def lt_all(others) + grouping_all :lt, others + end + + def lteq(right) + Nodes::LessThanOrEqual.new self, quoted_node(right) + end + + def lteq_any(others) + grouping_any :lteq, others + end + + def lteq_all(others) + grouping_all :lteq, others + end + + def when(right) + Nodes::Case.new(self).when quoted_node(right) + end + + def concat(other) + Nodes::Concat.new self, other + end + + private + + def grouping_any(method_id, others, *extras) + nodes = others.map { |expr| send(method_id, expr, *extras) } + Nodes::Grouping.new nodes.inject { |memo, node| + Nodes::Or.new(memo, node) + } + end + + def grouping_all(method_id, others, *extras) + nodes = others.map { |expr| send(method_id, expr, *extras) } + Nodes::Grouping.new Nodes::And.new(nodes) + end + + def quoted_node(other) + Nodes.build_quoted(other, self) + end + + def quoted_array(others) + others.map { |v| quoted_node(v) } + end + + def equals_quoted?(maybe_quoted, value) + if maybe_quoted.is_a?(Nodes::Quoted) + maybe_quoted.val == value + else + maybe_quoted == value + end + end + end +end diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb new file mode 100644 index 0000000000..22a04b00c6 --- /dev/null +++ b/activerecord/lib/arel/select_manager.rb @@ -0,0 +1,273 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class SelectManager < Arel::TreeManager + include Arel::Crud + + STRING_OR_SYMBOL_CLASS = [Symbol, String] + + def initialize(table = nil) + super() + @ast = Nodes::SelectStatement.new + @ctx = @ast.cores.last + from table + end + + def initialize_copy(other) + super + @ctx = @ast.cores.last + end + + def limit + @ast.limit && @ast.limit.expr + end + alias :taken :limit + + def constraints + @ctx.wheres + end + + def offset + @ast.offset && @ast.offset.expr + end + + def skip(amount) + if amount + @ast.offset = Nodes::Offset.new(amount) + else + @ast.offset = nil + end + self + end + alias :offset= :skip + + ### + # Produces an Arel::Nodes::Exists node + def exists + Arel::Nodes::Exists.new @ast + end + + def as(other) + create_table_alias grouping(@ast), Nodes::SqlLiteral.new(other) + end + + def lock(locking = Arel.sql("FOR UPDATE")) + case locking + when true + locking = Arel.sql("FOR UPDATE") + when Arel::Nodes::SqlLiteral + when String + locking = Arel.sql locking + end + + @ast.lock = Nodes::Lock.new(locking) + self + end + + def locked + @ast.lock + end + + def on(*exprs) + @ctx.source.right.last.right = Nodes::On.new(collapse(exprs)) + self + end + + def group(*columns) + columns.each do |column| + # FIXME: backwards compat + column = Nodes::SqlLiteral.new(column) if String === column + column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column + + @ctx.groups.push Nodes::Group.new column + end + self + end + + def from(table) + table = Nodes::SqlLiteral.new(table) if String === table + + case table + when Nodes::Join + @ctx.source.right << table + else + @ctx.source.left = table + end + + self + end + + def froms + @ast.cores.map { |x| x.from }.compact + end + + def join(relation, klass = Nodes::InnerJoin) + return self unless relation + + case relation + when String, Nodes::SqlLiteral + raise EmptyJoinError if relation.empty? + klass = Nodes::StringJoin + end + + @ctx.source.right << create_join(relation, nil, klass) + self + end + + def outer_join(relation) + join(relation, Nodes::OuterJoin) + end + + def having(expr) + @ctx.havings << expr + self + end + + def window(name) + window = Nodes::NamedWindow.new(name) + @ctx.windows.push window + window + end + + def project(*projections) + # FIXME: converting these to SQLLiterals is probably not good, but + # rails tests require it. + @ctx.projections.concat projections.map { |x| + STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x + } + self + end + + def projections + @ctx.projections + end + + def projections=(projections) + @ctx.projections = projections + end + + def distinct(value = true) + if value + @ctx.set_quantifier = Arel::Nodes::Distinct.new + else + @ctx.set_quantifier = nil + end + self + end + + def distinct_on(value) + if value + @ctx.set_quantifier = Arel::Nodes::DistinctOn.new(value) + else + @ctx.set_quantifier = nil + end + self + end + + def order(*expr) + # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically + @ast.orders.concat expr.map { |x| + STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x + } + self + end + + def orders + @ast.orders + end + + def where_sql(engine = Table.engine) + return if @ctx.wheres.empty? + + viz = Visitors::WhereSql.new(engine.connection.visitor, engine.connection) + Nodes::SqlLiteral.new viz.accept(@ctx, Collectors::SQLString.new).value + end + + def union(operation, other = nil) + if other + node_class = Nodes.const_get("Union#{operation.to_s.capitalize}") + else + other = operation + node_class = Nodes::Union + end + + node_class.new self.ast, other.ast + end + + def intersect(other) + Nodes::Intersect.new ast, other.ast + end + + def except(other) + Nodes::Except.new ast, other.ast + end + alias :minus :except + + def lateral(table_name = nil) + base = table_name.nil? ? ast : as(table_name) + Nodes::Lateral.new(base) + end + + def with(*subqueries) + if subqueries.first.is_a? Symbol + node_class = Nodes.const_get("With#{subqueries.shift.to_s.capitalize}") + else + node_class = Nodes::With + end + @ast.with = node_class.new(subqueries.flatten) + + self + end + + def take(limit) + if limit + @ast.limit = Nodes::Limit.new(limit) + @ctx.top = Nodes::Top.new(limit) + else + @ast.limit = nil + @ctx.top = nil + end + self + end + alias limit= take + + def join_sources + @ctx.source.right + end + + def source + @ctx.source + end + + class Row < Struct.new(:data) # :nodoc: + def id + data["id"] + end + + def method_missing(name, *args) + name = name.to_s + return data[name] if data.key?(name) + super + end + end + + private + def collapse(exprs, existing = nil) + exprs = exprs.unshift(existing.expr) if existing + exprs = exprs.compact.map { |expr| + if String === expr + # FIXME: Don't do this automatically + Arel.sql(expr) + else + expr + end + } + + if exprs.length == 1 + exprs.first + else + create_and exprs + end + end + end +end diff --git a/activerecord/lib/arel/table.rb b/activerecord/lib/arel/table.rb new file mode 100644 index 0000000000..686fcdf962 --- /dev/null +++ b/activerecord/lib/arel/table.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class Table + include Arel::Crud + include Arel::FactoryMethods + + @engine = nil + class << self; attr_accessor :engine; end + + attr_accessor :name, :table_alias + + # TableAlias and Table both have a #table_name which is the name of the underlying table + alias :table_name :name + + def initialize(name, as: nil, type_caster: nil) + @name = name.to_s + @type_caster = type_caster + + # Sometime AR sends an :as parameter to table, to let the table know + # that it is an Alias. We may want to override new, and return a + # TableAlias node? + if as.to_s == @name + as = nil + end + @table_alias = as + end + + def alias(name = "#{self.name}_2") + Nodes::TableAlias.new(self, name) + end + + def from + SelectManager.new(self) + end + + def join(relation, klass = Nodes::InnerJoin) + return from unless relation + + case relation + when String, Nodes::SqlLiteral + raise EmptyJoinError if relation.empty? + klass = Nodes::StringJoin + end + + from.join(relation, klass) + end + + def outer_join(relation) + join(relation, Nodes::OuterJoin) + end + + def group(*columns) + from.group(*columns) + end + + def order(*expr) + from.order(*expr) + end + + def where(condition) + from.where condition + end + + def project(*things) + from.project(*things) + end + + def take(amount) + from.take amount + end + + def skip(amount) + from.skip amount + end + + def having(expr) + from.having expr + end + + def [](name) + ::Arel::Attribute.new self, name + end + + def hash + # Perf note: aliases and table alias is excluded from the hash + # aliases can have a loop back to this table breaking hashes in parent + # relations, for the vast majority of cases @name is unique to a query + @name.hash + end + + def eql?(other) + self.class == other.class && + self.name == other.name && + self.table_alias == other.table_alias + end + alias :== :eql? + + def type_cast_for_database(attribute_name, value) + type_caster.type_cast_for_database(attribute_name, value) + end + + def able_to_type_cast? + !type_caster.nil? + end + + protected + + attr_reader :type_caster + end +end diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb new file mode 100644 index 0000000000..ed47b09a37 --- /dev/null +++ b/activerecord/lib/arel/tree_manager.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class TreeManager + include Arel::FactoryMethods + + attr_reader :ast + + def initialize + @ctx = nil + end + + def to_dot + collector = Arel::Collectors::PlainString.new + collector = Visitors::Dot.new.accept @ast, collector + collector.value + end + + def to_sql(engine = Table.engine) + collector = Arel::Collectors::SQLString.new + collector = engine.connection.visitor.accept @ast, collector + collector.value + end + + def initialize_copy(other) + super + @ast = @ast.clone + end + + def where(expr) + if Arel::TreeManager === expr + expr = expr.ast + end + @ctx.wheres << expr + self + end + end +end diff --git a/activerecord/lib/arel/update_manager.rb b/activerecord/lib/arel/update_manager.rb new file mode 100644 index 0000000000..fe444343ba --- /dev/null +++ b/activerecord/lib/arel/update_manager.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + class UpdateManager < Arel::TreeManager + def initialize + super + @ast = Nodes::UpdateStatement.new + @ctx = @ast + end + + def take(limit) + @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit + self + end + + def key=(key) + @ast.key = Nodes.build_quoted(key) + end + + def key + @ast.key + end + + def order(*expr) + @ast.orders = expr + self + end + + ### + # UPDATE +table+ + def table(table) + @ast.relation = table + self + end + + def wheres=(exprs) + @ast.wheres = exprs + end + + def where(expr) + @ast.wheres << expr + self + end + + def set(values) + if String === values + @ast.values = [values] + else + @ast.values = values.map { |column, value| + Nodes::Assignment.new( + Nodes::UnqualifiedColumn.new(column), + value + ) + } + end + self + end + end +end diff --git a/activerecord/lib/arel/visitors.rb b/activerecord/lib/arel/visitors.rb new file mode 100644 index 0000000000..e350f52e65 --- /dev/null +++ b/activerecord/lib/arel/visitors.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "arel/visitors/visitor" +require "arel/visitors/depth_first" +require "arel/visitors/to_sql" +require "arel/visitors/sqlite" +require "arel/visitors/postgresql" +require "arel/visitors/mysql" +require "arel/visitors/mssql" +require "arel/visitors/oracle" +require "arel/visitors/oracle12" +require "arel/visitors/where_sql" +require "arel/visitors/dot" +require "arel/visitors/ibm_db" +require "arel/visitors/informix" + +module Arel # :nodoc: all + module Visitors + end +end diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb new file mode 100644 index 0000000000..bcf8f8f980 --- /dev/null +++ b/activerecord/lib/arel/visitors/depth_first.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class DepthFirst < Arel::Visitors::Visitor + def initialize(block = nil) + @block = block || Proc.new + super() + end + + private + + def visit(o) + super + @block.call o + end + + def unary(o) + visit o.expr + end + alias :visit_Arel_Nodes_Else :unary + alias :visit_Arel_Nodes_Group :unary + alias :visit_Arel_Nodes_Cube :unary + alias :visit_Arel_Nodes_RollUp :unary + alias :visit_Arel_Nodes_GroupingSet :unary + alias :visit_Arel_Nodes_GroupingElement :unary + alias :visit_Arel_Nodes_Grouping :unary + alias :visit_Arel_Nodes_Having :unary + alias :visit_Arel_Nodes_Lateral :unary + alias :visit_Arel_Nodes_Limit :unary + alias :visit_Arel_Nodes_Not :unary + alias :visit_Arel_Nodes_Offset :unary + alias :visit_Arel_Nodes_On :unary + alias :visit_Arel_Nodes_Ordering :unary + alias :visit_Arel_Nodes_Ascending :unary + alias :visit_Arel_Nodes_Descending :unary + alias :visit_Arel_Nodes_Top :unary + alias :visit_Arel_Nodes_UnqualifiedColumn :unary + + def function(o) + visit o.expressions + visit o.alias + visit o.distinct + end + alias :visit_Arel_Nodes_Avg :function + alias :visit_Arel_Nodes_Exists :function + alias :visit_Arel_Nodes_Max :function + alias :visit_Arel_Nodes_Min :function + alias :visit_Arel_Nodes_Sum :function + + def visit_Arel_Nodes_NamedFunction(o) + visit o.name + visit o.expressions + visit o.distinct + visit o.alias + end + + def visit_Arel_Nodes_Count(o) + visit o.expressions + visit o.alias + visit o.distinct + end + + def visit_Arel_Nodes_Case(o) + visit o.case + visit o.conditions + visit o.default + end + + def nary(o) + o.children.each { |child| visit child } + end + alias :visit_Arel_Nodes_And :nary + + def binary(o) + visit o.left + visit o.right + end + alias :visit_Arel_Nodes_As :binary + alias :visit_Arel_Nodes_Assignment :binary + alias :visit_Arel_Nodes_Between :binary + alias :visit_Arel_Nodes_Concat :binary + alias :visit_Arel_Nodes_DeleteStatement :binary + alias :visit_Arel_Nodes_DoesNotMatch :binary + alias :visit_Arel_Nodes_Equality :binary + alias :visit_Arel_Nodes_FullOuterJoin :binary + alias :visit_Arel_Nodes_GreaterThan :binary + alias :visit_Arel_Nodes_GreaterThanOrEqual :binary + alias :visit_Arel_Nodes_In :binary + alias :visit_Arel_Nodes_InfixOperation :binary + alias :visit_Arel_Nodes_JoinSource :binary + alias :visit_Arel_Nodes_InnerJoin :binary + alias :visit_Arel_Nodes_LessThan :binary + alias :visit_Arel_Nodes_LessThanOrEqual :binary + alias :visit_Arel_Nodes_Matches :binary + alias :visit_Arel_Nodes_NotEqual :binary + alias :visit_Arel_Nodes_NotIn :binary + alias :visit_Arel_Nodes_NotRegexp :binary + alias :visit_Arel_Nodes_Or :binary + alias :visit_Arel_Nodes_OuterJoin :binary + alias :visit_Arel_Nodes_Regexp :binary + alias :visit_Arel_Nodes_RightOuterJoin :binary + alias :visit_Arel_Nodes_TableAlias :binary + alias :visit_Arel_Nodes_Values :binary + alias :visit_Arel_Nodes_When :binary + + def visit_Arel_Nodes_StringJoin(o) + visit o.left + end + + def visit_Arel_Attribute(o) + visit o.relation + visit o.name + end + alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute + alias :visit_Arel_Attributes_Float :visit_Arel_Attribute + alias :visit_Arel_Attributes_String :visit_Arel_Attribute + alias :visit_Arel_Attributes_Time :visit_Arel_Attribute + alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute + alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute + alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute + + def visit_Arel_Table(o) + visit o.name + end + + def terminal(o) + end + alias :visit_ActiveSupport_Multibyte_Chars :terminal + alias :visit_ActiveSupport_StringInquirer :terminal + alias :visit_Arel_Nodes_Lock :terminal + alias :visit_Arel_Nodes_Node :terminal + alias :visit_Arel_Nodes_SqlLiteral :terminal + alias :visit_Arel_Nodes_BindParam :terminal + alias :visit_Arel_Nodes_Window :terminal + alias :visit_Arel_Nodes_True :terminal + alias :visit_Arel_Nodes_False :terminal + alias :visit_BigDecimal :terminal + alias :visit_Bignum :terminal + alias :visit_Class :terminal + alias :visit_Date :terminal + alias :visit_DateTime :terminal + alias :visit_FalseClass :terminal + alias :visit_Fixnum :terminal + alias :visit_Float :terminal + alias :visit_Integer :terminal + alias :visit_NilClass :terminal + alias :visit_String :terminal + alias :visit_Symbol :terminal + alias :visit_Time :terminal + alias :visit_TrueClass :terminal + + def visit_Arel_Nodes_InsertStatement(o) + visit o.relation + visit o.columns + visit o.values + end + + def visit_Arel_Nodes_SelectCore(o) + visit o.projections + visit o.source + visit o.wheres + visit o.groups + visit o.windows + visit o.havings + end + + def visit_Arel_Nodes_SelectStatement(o) + visit o.cores + visit o.orders + visit o.limit + visit o.lock + visit o.offset + end + + def visit_Arel_Nodes_UpdateStatement(o) + visit o.relation + visit o.values + visit o.wheres + visit o.orders + visit o.limit + end + + def visit_Array(o) + o.each { |i| visit i } + end + alias :visit_Set :visit_Array + + def visit_Hash(o) + o.each { |k, v| visit(k); visit(v) } + end + + DISPATCH = dispatch_cache + + def get_dispatch_cache + DISPATCH + end + end + end +end diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb new file mode 100644 index 0000000000..d352b81914 --- /dev/null +++ b/activerecord/lib/arel/visitors/dot.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class Dot < Arel::Visitors::Visitor + class Node # :nodoc: + attr_accessor :name, :id, :fields + + def initialize(name, id, fields = []) + @name = name + @id = id + @fields = fields + end + end + + class Edge < Struct.new :name, :from, :to # :nodoc: + end + + def initialize + super() + @nodes = [] + @edges = [] + @node_stack = [] + @edge_stack = [] + @seen = {} + end + + def accept(object, collector) + visit object + collector << to_dot + end + + private + + def visit_Arel_Nodes_Ordering(o) + visit_edge o, "expr" + end + + def visit_Arel_Nodes_TableAlias(o) + visit_edge o, "name" + visit_edge o, "relation" + end + + def visit_Arel_Nodes_Count(o) + visit_edge o, "expressions" + visit_edge o, "distinct" + end + + def visit_Arel_Nodes_Values(o) + visit_edge o, "expressions" + end + + def visit_Arel_Nodes_StringJoin(o) + visit_edge o, "left" + end + + def visit_Arel_Nodes_InnerJoin(o) + visit_edge o, "left" + visit_edge o, "right" + end + alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_InnerJoin + alias :visit_Arel_Nodes_OuterJoin :visit_Arel_Nodes_InnerJoin + alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_InnerJoin + + def visit_Arel_Nodes_DeleteStatement(o) + visit_edge o, "relation" + visit_edge o, "wheres" + end + + def unary(o) + visit_edge o, "expr" + end + alias :visit_Arel_Nodes_Group :unary + alias :visit_Arel_Nodes_Cube :unary + alias :visit_Arel_Nodes_RollUp :unary + alias :visit_Arel_Nodes_GroupingSet :unary + alias :visit_Arel_Nodes_GroupingElement :unary + alias :visit_Arel_Nodes_Grouping :unary + alias :visit_Arel_Nodes_Having :unary + alias :visit_Arel_Nodes_Limit :unary + alias :visit_Arel_Nodes_Not :unary + alias :visit_Arel_Nodes_Offset :unary + alias :visit_Arel_Nodes_On :unary + alias :visit_Arel_Nodes_Top :unary + alias :visit_Arel_Nodes_UnqualifiedColumn :unary + alias :visit_Arel_Nodes_Preceding :unary + alias :visit_Arel_Nodes_Following :unary + alias :visit_Arel_Nodes_Rows :unary + alias :visit_Arel_Nodes_Range :unary + + def window(o) + visit_edge o, "partitions" + visit_edge o, "orders" + visit_edge o, "framing" + end + alias :visit_Arel_Nodes_Window :window + + def named_window(o) + visit_edge o, "partitions" + visit_edge o, "orders" + visit_edge o, "framing" + visit_edge o, "name" + end + alias :visit_Arel_Nodes_NamedWindow :named_window + + def function(o) + visit_edge o, "expressions" + visit_edge o, "distinct" + visit_edge o, "alias" + end + alias :visit_Arel_Nodes_Exists :function + alias :visit_Arel_Nodes_Min :function + alias :visit_Arel_Nodes_Max :function + alias :visit_Arel_Nodes_Avg :function + alias :visit_Arel_Nodes_Sum :function + + def extract(o) + visit_edge o, "expressions" + visit_edge o, "alias" + end + alias :visit_Arel_Nodes_Extract :extract + + def visit_Arel_Nodes_NamedFunction(o) + visit_edge o, "name" + visit_edge o, "expressions" + visit_edge o, "distinct" + visit_edge o, "alias" + end + + def visit_Arel_Nodes_InsertStatement(o) + visit_edge o, "relation" + visit_edge o, "columns" + visit_edge o, "values" + end + + def visit_Arel_Nodes_SelectCore(o) + visit_edge o, "source" + visit_edge o, "projections" + visit_edge o, "wheres" + visit_edge o, "windows" + end + + def visit_Arel_Nodes_SelectStatement(o) + visit_edge o, "cores" + visit_edge o, "limit" + visit_edge o, "orders" + visit_edge o, "offset" + end + + def visit_Arel_Nodes_UpdateStatement(o) + visit_edge o, "relation" + visit_edge o, "wheres" + visit_edge o, "values" + end + + def visit_Arel_Table(o) + visit_edge o, "name" + end + + def visit_Arel_Nodes_Casted(o) + visit_edge o, "val" + visit_edge o, "attribute" + end + + def visit_Arel_Attribute(o) + visit_edge o, "relation" + visit_edge o, "name" + end + alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute + alias :visit_Arel_Attributes_Float :visit_Arel_Attribute + alias :visit_Arel_Attributes_String :visit_Arel_Attribute + alias :visit_Arel_Attributes_Time :visit_Arel_Attribute + alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute + alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute + + def nary(o) + o.children.each_with_index do |x, i| + edge(i) { visit x } + end + end + alias :visit_Arel_Nodes_And :nary + + def binary(o) + visit_edge o, "left" + visit_edge o, "right" + end + alias :visit_Arel_Nodes_As :binary + alias :visit_Arel_Nodes_Assignment :binary + alias :visit_Arel_Nodes_Between :binary + alias :visit_Arel_Nodes_Concat :binary + alias :visit_Arel_Nodes_DoesNotMatch :binary + alias :visit_Arel_Nodes_Equality :binary + alias :visit_Arel_Nodes_GreaterThan :binary + alias :visit_Arel_Nodes_GreaterThanOrEqual :binary + alias :visit_Arel_Nodes_In :binary + alias :visit_Arel_Nodes_JoinSource :binary + alias :visit_Arel_Nodes_LessThan :binary + alias :visit_Arel_Nodes_LessThanOrEqual :binary + alias :visit_Arel_Nodes_Matches :binary + alias :visit_Arel_Nodes_NotEqual :binary + alias :visit_Arel_Nodes_NotIn :binary + alias :visit_Arel_Nodes_Or :binary + alias :visit_Arel_Nodes_Over :binary + + def visit_String(o) + @node_stack.last.fields << o + end + alias :visit_Time :visit_String + alias :visit_Date :visit_String + alias :visit_DateTime :visit_String + alias :visit_NilClass :visit_String + alias :visit_TrueClass :visit_String + alias :visit_FalseClass :visit_String + alias :visit_Integer :visit_String + alias :visit_Fixnum :visit_String + alias :visit_BigDecimal :visit_String + alias :visit_Float :visit_String + alias :visit_Symbol :visit_String + alias :visit_Arel_Nodes_SqlLiteral :visit_String + + def visit_Arel_Nodes_BindParam(o); end + + def visit_Hash(o) + o.each_with_index do |pair, i| + edge("pair_#{i}") { visit pair } + end + end + + def visit_Array(o) + o.each_with_index do |x, i| + edge(i) { visit x } + end + end + alias :visit_Set :visit_Array + + def visit_edge(o, method) + edge(method) { visit o.send(method) } + end + + def visit(o) + if node = @seen[o.object_id] + @edge_stack.last.to = node + return + end + + node = Node.new(o.class.name, o.object_id) + @seen[node.id] = node + @nodes << node + with_node node do + super + end + end + + def edge(name) + edge = Edge.new(name, @node_stack.last) + @edge_stack.push edge + @edges << edge + yield + @edge_stack.pop + end + + def with_node(node) + if edge = @edge_stack.last + edge.to = node + end + + @node_stack.push node + yield + @node_stack.pop + end + + def quote(string) + string.to_s.gsub('"', '\"') + end + + def to_dot + "digraph \"Arel\" {\nnode [width=0.375,height=0.25,shape=record];\n" + + @nodes.map { |node| + label = "<f0>#{node.name}" + + node.fields.each_with_index do |field, i| + label += "|<f#{i + 1}>#{quote field}" + end + + "#{node.id} [label=\"#{label}\"];" + }.join("\n") + "\n" + @edges.map { |edge| + "#{edge.from.id} -> #{edge.to.id} [label=\"#{edge.name}\"];" + }.join("\n") + "\n}" + end + end + end +end diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb new file mode 100644 index 0000000000..0a06aef60b --- /dev/null +++ b/activerecord/lib/arel/visitors/ibm_db.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class IBM_DB < Arel::Visitors::ToSql + private + + def visit_Arel_Nodes_Limit(o, collector) + collector << "FETCH FIRST " + collector = visit o.expr, collector + collector << " ROWS ONLY" + end + end + end +end diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb new file mode 100644 index 0000000000..0a9713794e --- /dev/null +++ b/activerecord/lib/arel/visitors/informix.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class Informix < Arel::Visitors::ToSql + private + def visit_Arel_Nodes_SelectStatement(o, collector) + collector << "SELECT " + collector = maybe_visit o.offset, collector + collector = maybe_visit o.limit, collector + collector = o.cores.inject(collector) { |c, x| + visit_Arel_Nodes_SelectCore x, c + } + if o.orders.any? + collector << "ORDER BY " + collector = inject_join o.orders, collector, ", " + end + collector = maybe_visit o.lock, collector + end + def visit_Arel_Nodes_SelectCore(o, collector) + collector = inject_join o.projections, collector, ", " + if o.source && !o.source.empty? + collector << " FROM " + collector = visit o.source, collector + end + + if o.wheres.any? + collector << " WHERE " + collector = inject_join o.wheres, collector, " AND " + end + + if o.groups.any? + collector << "GROUP BY " + collector = inject_join o.groups, collector, ", " + end + + if o.havings.any? + collector << " HAVING " + collector = inject_join o.havings, collector, " AND " + end + collector + end + + def visit_Arel_Nodes_Offset(o, collector) + collector << "SKIP " + visit o.expr, collector + end + def visit_Arel_Nodes_Limit(o, collector) + collector << "FIRST " + visit o.expr, collector + collector << " " + end + end + end +end diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb new file mode 100644 index 0000000000..9aedc51d15 --- /dev/null +++ b/activerecord/lib/arel/visitors/mssql.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class MSSQL < Arel::Visitors::ToSql + RowNumber = Struct.new :children + + def initialize(*) + @primary_keys = {} + super + end + + private + + # `top` wouldn't really work here. I.e. User.select("distinct first_name").limit(10) would generate + # "select top 10 distinct first_name from users", which is invalid query! it should be + # "select distinct top 10 first_name from users" + def visit_Arel_Nodes_Top(o) + "" + end + + def visit_Arel_Visitors_MSSQL_RowNumber(o, collector) + collector << "ROW_NUMBER() OVER (ORDER BY " + inject_join(o.children, collector, ", ") << ") as _row_num" + end + + def visit_Arel_Nodes_SelectStatement(o, collector) + if !o.limit && !o.offset + return super + end + + is_select_count = false + o.cores.each { |x| + core_order_by = row_num_literal determine_order_by(o.orders, x) + if select_count? x + x.projections = [core_order_by] + is_select_count = true + else + x.projections << core_order_by + end + } + + if is_select_count + # fixme count distinct wouldn't work with limit or offset + collector << "SELECT COUNT(1) as count_id FROM (" + end + + collector << "SELECT _t.* FROM (" + collector = o.cores.inject(collector) { |c, x| + visit_Arel_Nodes_SelectCore x, c + } + collector << ") as _t WHERE #{get_offset_limit_clause(o)}" + + if is_select_count + collector << ") AS subquery" + else + collector + end + end + + def get_offset_limit_clause(o) + first_row = o.offset ? o.offset.expr.to_i + 1 : 1 + last_row = o.limit ? o.limit.expr.to_i - 1 + first_row : nil + if last_row + " _row_num BETWEEN #{first_row} AND #{last_row}" + else + " _row_num >= #{first_row}" + end + end + + def visit_Arel_Nodes_DeleteStatement(o, collector) + collector << "DELETE " + if o.limit + collector << "TOP (" + visit o.limit.expr, collector + collector << ") " + end + collector << "FROM " + collector = visit o.relation, collector + if o.wheres.any? + collector << " WHERE " + inject_join o.wheres, collector, AND + else + collector + end + end + + def determine_order_by(orders, x) + if orders.any? + orders + elsif x.groups.any? + x.groups + else + pk = find_left_table_pk(x.froms) + pk ? [pk] : [] + end + end + + def row_num_literal(order_by) + RowNumber.new order_by + end + + def select_count?(x) + x.projections.length == 1 && Arel::Nodes::Count === x.projections.first + end + + # FIXME raise exception of there is no pk? + def find_left_table_pk(o) + if o.kind_of?(Arel::Nodes::Join) + find_left_table_pk(o.left) + elsif o.instance_of?(Arel::Table) + find_primary_key(o) + end + end + + def find_primary_key(o) + @primary_keys[o.name] ||= begin + primary_key_name = @connection.primary_key(o.name) + # some tables might be without primary key + primary_key_name && o[primary_key_name] + end + end + end + end +end diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb new file mode 100644 index 0000000000..37bfb661f0 --- /dev/null +++ b/activerecord/lib/arel/visitors/mysql.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class MySQL < Arel::Visitors::ToSql + private + def visit_Arel_Nodes_Union(o, collector, suppress_parens = false) + unless suppress_parens + collector << "( " + end + + collector = case o.left + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.left, collector, true + else + visit o.left, collector + end + + collector << " UNION " + + collector = case o.right + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.right, collector, true + else + visit o.right, collector + end + + if suppress_parens + collector + else + collector << " )" + end + end + + def visit_Arel_Nodes_Bin(o, collector) + collector << "BINARY " + visit o.expr, collector + end + + ### + # :'( + # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214 + def visit_Arel_Nodes_SelectStatement(o, collector) + if o.offset && !o.limit + o.limit = Arel::Nodes::Limit.new(18446744073709551615) + end + super + end + + def visit_Arel_Nodes_SelectCore(o, collector) + o.froms ||= Arel.sql("DUAL") + super + end + + def visit_Arel_Nodes_UpdateStatement(o, collector) + collector << "UPDATE " + collector = visit o.relation, collector + + unless o.values.empty? + collector << " SET " + collector = inject_join o.values, collector, ", " + end + + unless o.wheres.empty? + collector << " WHERE " + collector = inject_join o.wheres, collector, " AND " + end + + unless o.orders.empty? + collector << " ORDER BY " + collector = inject_join o.orders, collector, ", " + end + + maybe_visit o.limit, collector + end + + def visit_Arel_Nodes_Concat(o, collector) + collector << " CONCAT(" + visit o.left, collector + collector << ", " + visit o.right, collector + collector << ") " + collector + end + end + end +end diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb new file mode 100644 index 0000000000..30a1529d46 --- /dev/null +++ b/activerecord/lib/arel/visitors/oracle.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class Oracle < Arel::Visitors::ToSql + private + + def visit_Arel_Nodes_SelectStatement(o, collector) + o = order_hacks(o) + + # if need to select first records without ORDER BY and GROUP BY and without DISTINCT + # then can use simple ROWNUM in WHERE clause + if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && o.cores.first.set_quantifier.class.to_s !~ /Distinct/ + o.cores.last.wheres.push Nodes::LessThanOrEqual.new( + Nodes::SqlLiteral.new("ROWNUM"), o.limit.expr + ) + return super + end + + if o.limit && o.offset + o = o.dup + limit = o.limit.expr + offset = o.offset + o.offset = nil + collector << " + SELECT * FROM ( + SELECT raw_sql_.*, rownum raw_rnum_ + FROM (" + + collector = super(o, collector) + + if offset.expr.is_a? Nodes::BindParam + collector << ") raw_sql_ WHERE rownum <= (" + collector = visit offset.expr, collector + collector << " + " + collector = visit limit, collector + collector << ") ) WHERE raw_rnum_ > " + collector = visit offset.expr, collector + return collector + else + collector << ") raw_sql_ + WHERE rownum <= #{offset.expr.to_i + limit} + ) + WHERE " + return visit(offset, collector) + end + end + + if o.limit + o = o.dup + limit = o.limit.expr + collector << "SELECT * FROM (" + collector = super(o, collector) + collector << ") WHERE ROWNUM <= " + return visit limit, collector + end + + if o.offset + o = o.dup + offset = o.offset + o.offset = nil + collector << "SELECT * FROM ( + SELECT raw_sql_.*, rownum raw_rnum_ + FROM (" + collector = super(o, collector) + collector << ") raw_sql_ + ) + WHERE " + return visit offset, collector + end + + super + end + + def visit_Arel_Nodes_Limit(o, collector) + collector + end + + def visit_Arel_Nodes_Offset(o, collector) + collector << "raw_rnum_ > " + visit o.expr, collector + end + + def visit_Arel_Nodes_Except(o, collector) + collector << "( " + collector = infix_value o, collector, " MINUS " + collector << " )" + end + + def visit_Arel_Nodes_UpdateStatement(o, collector) + # Oracle does not allow ORDER BY/LIMIT in UPDATEs. + if o.orders.any? && o.limit.nil? + # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided, + # otherwise let the user deal with the error + o = o.dup + o.orders = [] + end + + super + end + + ### + # Hacks for the order clauses specific to Oracle + def order_hacks(o) + return o if o.orders.empty? + return o unless o.cores.any? do |core| + core.projections.any? do |projection| + /FIRST_VALUE/ === projection + end + end + # Previous version with join and split broke ORDER BY clause + # if it contained functions with several arguments (separated by ','). + # + # orders = o.orders.map { |x| visit x }.join(', ').split(',') + orders = o.orders.map do |x| + string = visit(x, Arel::Collectors::SQLString.new).value + if string.include?(",") + split_order_string(string) + else + string + end + end.flatten + o.orders = [] + orders.each_with_index do |order, i| + o.orders << + Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i === order}") + end + o + end + + # Split string by commas but count opening and closing brackets + # and ignore commas inside brackets. + def split_order_string(string) + array = [] + i = 0 + string.split(",").each do |part| + if array[i] + array[i] << "," << part + else + # to ensure that array[i] will be String and not Arel::Nodes::SqlLiteral + array[i] = part.to_s + end + i += 1 if array[i].count("(") == array[i].count(")") + end + array + end + + def visit_Arel_Nodes_BindParam(o, collector) + collector.add_bind(o.value) { |i| ":a#{i}" } + end + end + end +end diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb new file mode 100644 index 0000000000..7061f06087 --- /dev/null +++ b/activerecord/lib/arel/visitors/oracle12.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class Oracle12 < Arel::Visitors::ToSql + private + + def visit_Arel_Nodes_SelectStatement(o, collector) + # Oracle does not allow LIMIT clause with select for update + if o.limit && o.lock + raise ArgumentError, <<-MSG + 'Combination of limit and lock is not supported. + because generated SQL statements + `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014.` + MSG + end + super + end + + def visit_Arel_Nodes_SelectOptions(o, collector) + collector = maybe_visit o.offset, collector + collector = maybe_visit o.limit, collector + collector = maybe_visit o.lock, collector + end + + def visit_Arel_Nodes_Limit(o, collector) + collector << "FETCH FIRST " + collector = visit o.expr, collector + collector << " ROWS ONLY" + end + + def visit_Arel_Nodes_Offset(o, collector) + collector << "OFFSET " + visit o.expr, collector + collector << " ROWS" + end + + def visit_Arel_Nodes_Except(o, collector) + collector << "( " + collector = infix_value o, collector, " MINUS " + collector << " )" + end + + def visit_Arel_Nodes_UpdateStatement(o, collector) + # Oracle does not allow ORDER BY/LIMIT in UPDATEs. + if o.orders.any? && o.limit.nil? + # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided, + # otherwise let the user deal with the error + o = o.dup + o.orders = [] + end + + super + end + + def visit_Arel_Nodes_BindParam(o, collector) + collector.add_bind(o.value) { |i| ":a#{i}" } + end + end + end +end diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb new file mode 100644 index 0000000000..c5110fa89c --- /dev/null +++ b/activerecord/lib/arel/visitors/postgresql.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class PostgreSQL < Arel::Visitors::ToSql + CUBE = "CUBE" + ROLLUP = "ROLLUP" + GROUPING_SETS = "GROUPING SETS" + LATERAL = "LATERAL" + + private + + def visit_Arel_Nodes_Matches(o, collector) + op = o.case_sensitive ? " LIKE " : " ILIKE " + collector = infix_value o, collector, op + if o.escape + collector << " ESCAPE " + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_DoesNotMatch(o, collector) + op = o.case_sensitive ? " NOT LIKE " : " NOT ILIKE " + collector = infix_value o, collector, op + if o.escape + collector << " ESCAPE " + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_Regexp(o, collector) + op = o.case_sensitive ? " ~ " : " ~* " + infix_value o, collector, op + end + + def visit_Arel_Nodes_NotRegexp(o, collector) + op = o.case_sensitive ? " !~ " : " !~* " + infix_value o, collector, op + end + + def visit_Arel_Nodes_DistinctOn(o, collector) + collector << "DISTINCT ON ( " + visit(o.expr, collector) << " )" + end + + def visit_Arel_Nodes_BindParam(o, collector) + collector.add_bind(o.value) { |i| "$#{i}" } + end + + def visit_Arel_Nodes_GroupingElement(o, collector) + collector << "( " + visit(o.expr, collector) << " )" + end + + def visit_Arel_Nodes_Cube(o, collector) + collector << CUBE + grouping_array_or_grouping_element o, collector + end + + def visit_Arel_Nodes_RollUp(o, collector) + collector << ROLLUP + grouping_array_or_grouping_element o, collector + end + + def visit_Arel_Nodes_GroupingSet(o, collector) + collector << GROUPING_SETS + grouping_array_or_grouping_element o, collector + end + + def visit_Arel_Nodes_Lateral(o, collector) + collector << LATERAL + collector << SPACE + grouping_parentheses o, collector + end + + # Used by Lateral visitor to enclose select queries in parentheses + def grouping_parentheses(o, collector) + if o.expr.is_a? Nodes::SelectStatement + collector << "(" + visit o.expr, collector + collector << ")" + else + visit o.expr, collector + end + end + + # Utilized by GroupingSet, Cube & RollUp visitors to + # handle grouping aggregation semantics + def grouping_array_or_grouping_element(o, collector) + if o.expr.is_a? Array + collector << "( " + visit o.expr, collector + collector << " )" + else + visit o.expr, collector + end + end + end + end +end diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb new file mode 100644 index 0000000000..cb1d2424ad --- /dev/null +++ b/activerecord/lib/arel/visitors/sqlite.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class SQLite < Arel::Visitors::ToSql + private + + # Locks are not supported in SQLite + def visit_Arel_Nodes_Lock(o, collector) + collector + end + + def visit_Arel_Nodes_SelectStatement(o, collector) + o.limit = Arel::Nodes::Limit.new(-1) if o.offset && !o.limit + super + end + + def visit_Arel_Nodes_True(o, collector) + collector << "1" + end + + def visit_Arel_Nodes_False(o, collector) + collector << "0" + end + end + end +end diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb new file mode 100644 index 0000000000..5986fd5576 --- /dev/null +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -0,0 +1,847 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class UnsupportedVisitError < StandardError + def initialize(object) + super "Unsupported argument type: #{object.class.name}. Construct an Arel node instead." + end + end + + class ToSql < Arel::Visitors::Visitor + ## + # This is some roflscale crazy stuff. I'm roflscaling this because + # building SQL queries is a hotspot. I will explain the roflscale so that + # others will not rm this code. + # + # In YARV, string literals in a method body will get duped when the byte + # code is executed. Let's take a look: + # + # > puts RubyVM::InstructionSequence.new('def foo; "bar"; end').disasm + # + # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>===== + # 0000 trace 8 + # 0002 trace 1 + # 0004 putstring "bar" + # 0006 trace 16 + # 0008 leave + # + # The `putstring` bytecode will dup the string and push it on the stack. + # In many cases in our SQL visitor, that string is never mutated, so there + # is no need to dup the literal. + # + # If we change to a constant lookup, the string will not be duped, and we + # can reduce the objects in our system: + # + # > puts RubyVM::InstructionSequence.new('BAR = "bar"; def foo; BAR; end').disasm + # + # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>======== + # 0000 trace 8 + # 0002 trace 1 + # 0004 getinlinecache 11, <ic:0> + # 0007 getconstant :BAR + # 0009 setinlinecache <ic:0> + # 0011 trace 16 + # 0013 leave + # + # `getconstant` should be a hash lookup, and no object is duped when the + # value of the constant is pushed on the stack. Hence the crazy + # constants below. + # + # `matches` and `doesNotMatch` operate case-insensitively via Visitor subclasses + # specialized for specific databases when necessary. + # + + WHERE = " WHERE " # :nodoc: + SPACE = " " # :nodoc: + COMMA = ", " # :nodoc: + GROUP_BY = " GROUP BY " # :nodoc: + ORDER_BY = " ORDER BY " # :nodoc: + WINDOW = " WINDOW " # :nodoc: + AND = " AND " # :nodoc: + + DISTINCT = "DISTINCT" # :nodoc: + + def initialize(connection) + super() + @connection = connection + end + + def compile(node, &block) + accept(node, Arel::Collectors::SQLString.new, &block).value + end + + private + + def visit_Arel_Nodes_DeleteStatement(o, collector) + collector << "DELETE FROM " + collector = visit o.relation, collector + if o.wheres.any? + collector << WHERE + collector = inject_join o.wheres, collector, AND + end + + maybe_visit o.limit, collector + end + + # FIXME: we should probably have a 2-pass visitor for this + def build_subselect(key, o) + stmt = Nodes::SelectStatement.new + core = stmt.cores.first + core.froms = o.relation + core.wheres = o.wheres + core.projections = [key] + stmt.limit = o.limit + stmt.orders = o.orders + stmt + end + + def visit_Arel_Nodes_UpdateStatement(o, collector) + if o.orders.empty? && o.limit.nil? + wheres = o.wheres + else + wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])] + end + + collector << "UPDATE " + collector = visit o.relation, collector + unless o.values.empty? + collector << " SET " + collector = inject_join o.values, collector, ", " + end + + unless wheres.empty? + collector << " WHERE " + collector = inject_join wheres, collector, " AND " + end + + collector + end + + def visit_Arel_Nodes_InsertStatement(o, collector) + collector << "INSERT INTO " + collector = visit o.relation, collector + if o.columns.any? + collector << " (#{o.columns.map { |x| + quote_column_name x.name + }.join ', '})" + end + + if o.values + maybe_visit o.values, collector + elsif o.select + maybe_visit o.select, collector + else + collector + end + end + + def visit_Arel_Nodes_Exists(o, collector) + collector << "EXISTS (" + collector = visit(o.expressions, collector) << ")" + if o.alias + collector << " AS " + visit o.alias, collector + else + collector + end + end + + def visit_Arel_Nodes_Casted(o, collector) + collector << quoted(o.val, o.attribute).to_s + end + + def visit_Arel_Nodes_Quoted(o, collector) + collector << quoted(o.expr, nil).to_s + end + + def visit_Arel_Nodes_True(o, collector) + collector << "TRUE" + end + + def visit_Arel_Nodes_False(o, collector) + collector << "FALSE" + end + + def visit_Arel_Nodes_ValuesList(o, collector) + collector << "VALUES " + + len = o.rows.length - 1 + o.rows.each_with_index { |row, i| + collector << "(" + row_len = row.length - 1 + row.each_with_index do |value, k| + case value + when Nodes::SqlLiteral, Nodes::BindParam + collector = visit(value, collector) + else + collector << quote(value) + end + collector << COMMA unless k == row_len + end + collector << ")" + collector << COMMA unless i == len + } + collector + end + + def visit_Arel_Nodes_Values(o, collector) + collector << "VALUES (" + + len = o.expressions.length - 1 + o.expressions.each_with_index { |value, i| + case value + when Nodes::SqlLiteral, Nodes::BindParam + collector = visit value, collector + else + collector << quote(value).to_s + end + unless i == len + collector << COMMA + end + } + + collector << ")" + end + + def visit_Arel_Nodes_SelectStatement(o, collector) + if o.with + collector = visit o.with, collector + collector << SPACE + end + + collector = o.cores.inject(collector) { |c, x| + visit_Arel_Nodes_SelectCore(x, c) + } + + unless o.orders.empty? + collector << ORDER_BY + len = o.orders.length - 1 + o.orders.each_with_index { |x, i| + collector = visit(x, collector) + collector << COMMA unless len == i + } + end + + visit_Arel_Nodes_SelectOptions(o, collector) + + collector + end + + def visit_Arel_Nodes_SelectOptions(o, collector) + collector = maybe_visit o.limit, collector + collector = maybe_visit o.offset, collector + collector = maybe_visit o.lock, collector + end + + def visit_Arel_Nodes_SelectCore(o, collector) + collector << "SELECT" + + collector = maybe_visit o.top, collector + + collector = maybe_visit o.set_quantifier, collector + + collect_nodes_for o.projections, collector, SPACE + + if o.source && !o.source.empty? + collector << " FROM " + collector = visit o.source, collector + end + + collect_nodes_for o.wheres, collector, WHERE, AND + collect_nodes_for o.groups, collector, GROUP_BY + unless o.havings.empty? + collector << " HAVING " + inject_join o.havings, collector, AND + end + collect_nodes_for o.windows, collector, WINDOW + + collector + end + + def collect_nodes_for(nodes, collector, spacer, connector = COMMA) + unless nodes.empty? + collector << spacer + len = nodes.length - 1 + nodes.each_with_index do |x, i| + collector = visit(x, collector) + collector << connector unless len == i + end + end + end + + def visit_Arel_Nodes_Bin(o, collector) + visit o.expr, collector + end + + def visit_Arel_Nodes_Distinct(o, collector) + collector << DISTINCT + end + + def visit_Arel_Nodes_DistinctOn(o, collector) + raise NotImplementedError, "DISTINCT ON not implemented for this db" + end + + def visit_Arel_Nodes_With(o, collector) + collector << "WITH " + inject_join o.children, collector, COMMA + end + + def visit_Arel_Nodes_WithRecursive(o, collector) + collector << "WITH RECURSIVE " + inject_join o.children, collector, COMMA + end + + def visit_Arel_Nodes_Union(o, collector) + collector << "( " + infix_value(o, collector, " UNION ") << " )" + end + + def visit_Arel_Nodes_UnionAll(o, collector) + collector << "( " + infix_value(o, collector, " UNION ALL ") << " )" + end + + def visit_Arel_Nodes_Intersect(o, collector) + collector << "( " + infix_value(o, collector, " INTERSECT ") << " )" + end + + def visit_Arel_Nodes_Except(o, collector) + collector << "( " + infix_value(o, collector, " EXCEPT ") << " )" + end + + def visit_Arel_Nodes_NamedWindow(o, collector) + collector << quote_column_name(o.name) + collector << " AS " + visit_Arel_Nodes_Window o, collector + end + + def visit_Arel_Nodes_Window(o, collector) + collector << "(" + + if o.partitions.any? + collector << "PARTITION BY " + collector = inject_join o.partitions, collector, ", " + end + + if o.orders.any? + collector << SPACE if o.partitions.any? + collector << "ORDER BY " + collector = inject_join o.orders, collector, ", " + end + + if o.framing + collector << SPACE if o.partitions.any? || o.orders.any? + collector = visit o.framing, collector + end + + collector << ")" + end + + def visit_Arel_Nodes_Rows(o, collector) + if o.expr + collector << "ROWS " + visit o.expr, collector + else + collector << "ROWS" + end + end + + def visit_Arel_Nodes_Range(o, collector) + if o.expr + collector << "RANGE " + visit o.expr, collector + else + collector << "RANGE" + end + end + + def visit_Arel_Nodes_Preceding(o, collector) + collector = if o.expr + visit o.expr, collector + else + collector << "UNBOUNDED" + end + + collector << " PRECEDING" + end + + def visit_Arel_Nodes_Following(o, collector) + collector = if o.expr + visit o.expr, collector + else + collector << "UNBOUNDED" + end + + collector << " FOLLOWING" + end + + def visit_Arel_Nodes_CurrentRow(o, collector) + collector << "CURRENT ROW" + end + + def visit_Arel_Nodes_Over(o, collector) + case o.right + when nil + visit(o.left, collector) << " OVER ()" + when Arel::Nodes::SqlLiteral + infix_value o, collector, " OVER " + when String, Symbol + visit(o.left, collector) << " OVER #{quote_column_name o.right.to_s}" + else + infix_value o, collector, " OVER " + end + end + + def visit_Arel_Nodes_Offset(o, collector) + collector << "OFFSET " + visit o.expr, collector + end + + def visit_Arel_Nodes_Limit(o, collector) + collector << "LIMIT " + visit o.expr, collector + end + + # FIXME: this does nothing on most databases, but does on MSSQL + def visit_Arel_Nodes_Top(o, collector) + collector + end + + def visit_Arel_Nodes_Lock(o, collector) + visit o.expr, collector + end + + def visit_Arel_Nodes_Grouping(o, collector) + if o.expr.is_a? Nodes::Grouping + visit(o.expr, collector) + else + collector << "(" + visit(o.expr, collector) << ")" + end + end + + def visit_Arel_SelectManager(o, collector) + collector << "(" + visit(o.ast, collector) << ")" + end + + def visit_Arel_Nodes_Ascending(o, collector) + visit(o.expr, collector) << " ASC" + end + + def visit_Arel_Nodes_Descending(o, collector) + visit(o.expr, collector) << " DESC" + end + + def visit_Arel_Nodes_Group(o, collector) + visit o.expr, collector + end + + def visit_Arel_Nodes_NamedFunction(o, collector) + collector << o.name + collector << "(" + collector << "DISTINCT " if o.distinct + collector = inject_join(o.expressions, collector, ", ") << ")" + if o.alias + collector << " AS " + visit o.alias, collector + else + collector + end + end + + def visit_Arel_Nodes_Extract(o, collector) + collector << "EXTRACT(#{o.field.to_s.upcase} FROM " + visit(o.expr, collector) << ")" + end + + def visit_Arel_Nodes_Count(o, collector) + aggregate "COUNT", o, collector + end + + def visit_Arel_Nodes_Sum(o, collector) + aggregate "SUM", o, collector + end + + def visit_Arel_Nodes_Max(o, collector) + aggregate "MAX", o, collector + end + + def visit_Arel_Nodes_Min(o, collector) + aggregate "MIN", o, collector + end + + def visit_Arel_Nodes_Avg(o, collector) + aggregate "AVG", o, collector + end + + def visit_Arel_Nodes_TableAlias(o, collector) + collector = visit o.relation, collector + collector << " " + collector << quote_table_name(o.name) + end + + def visit_Arel_Nodes_Between(o, collector) + collector = visit o.left, collector + collector << " BETWEEN " + visit o.right, collector + end + + def visit_Arel_Nodes_GreaterThanOrEqual(o, collector) + collector = visit o.left, collector + collector << " >= " + visit o.right, collector + end + + def visit_Arel_Nodes_GreaterThan(o, collector) + collector = visit o.left, collector + collector << " > " + visit o.right, collector + end + + def visit_Arel_Nodes_LessThanOrEqual(o, collector) + collector = visit o.left, collector + collector << " <= " + visit o.right, collector + end + + def visit_Arel_Nodes_LessThan(o, collector) + collector = visit o.left, collector + collector << " < " + visit o.right, collector + end + + def visit_Arel_Nodes_Matches(o, collector) + collector = visit o.left, collector + collector << " LIKE " + collector = visit o.right, collector + if o.escape + collector << " ESCAPE " + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_DoesNotMatch(o, collector) + collector = visit o.left, collector + collector << " NOT LIKE " + collector = visit o.right, collector + if o.escape + collector << " ESCAPE " + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_JoinSource(o, collector) + if o.left + collector = visit o.left, collector + end + if o.right.any? + collector << SPACE if o.left + collector = inject_join o.right, collector, SPACE + end + collector + end + + def visit_Arel_Nodes_Regexp(o, collector) + raise NotImplementedError, "~ not implemented for this db" + end + + def visit_Arel_Nodes_NotRegexp(o, collector) + raise NotImplementedError, "!~ not implemented for this db" + end + + def visit_Arel_Nodes_StringJoin(o, collector) + visit o.left, collector + end + + def visit_Arel_Nodes_FullOuterJoin(o, collector) + collector << "FULL OUTER JOIN " + collector = visit o.left, collector + collector << SPACE + visit o.right, collector + end + + def visit_Arel_Nodes_OuterJoin(o, collector) + collector << "LEFT OUTER JOIN " + collector = visit o.left, collector + collector << " " + visit o.right, collector + end + + def visit_Arel_Nodes_RightOuterJoin(o, collector) + collector << "RIGHT OUTER JOIN " + collector = visit o.left, collector + collector << SPACE + visit o.right, collector + end + + def visit_Arel_Nodes_InnerJoin(o, collector) + collector << "INNER JOIN " + collector = visit o.left, collector + if o.right + collector << SPACE + visit(o.right, collector) + else + collector + end + end + + def visit_Arel_Nodes_On(o, collector) + collector << "ON " + visit o.expr, collector + end + + def visit_Arel_Nodes_Not(o, collector) + collector << "NOT (" + visit(o.expr, collector) << ")" + end + + def visit_Arel_Table(o, collector) + if o.table_alias + collector << "#{quote_table_name o.name} #{quote_table_name o.table_alias}" + else + collector << quote_table_name(o.name) + end + end + + def visit_Arel_Nodes_In(o, collector) + if Array === o.right && o.right.empty? + collector << "1=0" + else + collector = visit o.left, collector + collector << " IN (" + visit(o.right, collector) << ")" + end + end + + def visit_Arel_Nodes_NotIn(o, collector) + if Array === o.right && o.right.empty? + collector << "1=1" + else + collector = visit o.left, collector + collector << " NOT IN (" + collector = visit o.right, collector + collector << ")" + end + end + + def visit_Arel_Nodes_And(o, collector) + inject_join o.children, collector, " AND " + end + + def visit_Arel_Nodes_Or(o, collector) + collector = visit o.left, collector + collector << " OR " + visit o.right, collector + end + + def visit_Arel_Nodes_Assignment(o, collector) + case o.right + when Arel::Nodes::UnqualifiedColumn, Arel::Attributes::Attribute, Arel::Nodes::BindParam + collector = visit o.left, collector + collector << " = " + visit o.right, collector + else + collector = visit o.left, collector + collector << " = " + collector << quote(o.right).to_s + end + end + + def visit_Arel_Nodes_Equality(o, collector) + right = o.right + + collector = visit o.left, collector + + if right.nil? + collector << " IS NULL" + else + collector << " = " + visit right, collector + end + end + + def visit_Arel_Nodes_NotEqual(o, collector) + right = o.right + + collector = visit o.left, collector + + if right.nil? + collector << " IS NOT NULL" + else + collector << " != " + visit right, collector + end + end + + def visit_Arel_Nodes_As(o, collector) + collector = visit o.left, collector + collector << " AS " + visit o.right, collector + end + + def visit_Arel_Nodes_Case(o, collector) + collector << "CASE " + if o.case + visit o.case, collector + collector << " " + end + o.conditions.each do |condition| + visit condition, collector + collector << " " + end + if o.default + visit o.default, collector + collector << " " + end + collector << "END" + end + + def visit_Arel_Nodes_When(o, collector) + collector << "WHEN " + visit o.left, collector + collector << " THEN " + visit o.right, collector + end + + def visit_Arel_Nodes_Else(o, collector) + collector << "ELSE " + visit o.expr, collector + end + + def visit_Arel_Nodes_UnqualifiedColumn(o, collector) + collector << "#{quote_column_name o.name}" + collector + end + + def visit_Arel_Attributes_Attribute(o, collector) + join_name = o.relation.table_alias || o.relation.name + collector << "#{quote_table_name join_name}.#{quote_column_name o.name}" + end + alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute + + def literal(o, collector); collector << o.to_s; end + + def visit_Arel_Nodes_BindParam(o, collector) + collector.add_bind(o.value) { "?" } + end + + alias :visit_Arel_Nodes_SqlLiteral :literal + alias :visit_Bignum :literal + alias :visit_Fixnum :literal + alias :visit_Integer :literal + + def quoted(o, a) + if a && a.able_to_type_cast? + quote(a.type_cast_for_database(o)) + else + quote(o) + end + end + + def unsupported(o, collector) + raise UnsupportedVisitError.new(o) + end + + alias :visit_ActiveSupport_Multibyte_Chars :unsupported + alias :visit_ActiveSupport_StringInquirer :unsupported + alias :visit_BigDecimal :unsupported + alias :visit_Class :unsupported + alias :visit_Date :unsupported + alias :visit_DateTime :unsupported + alias :visit_FalseClass :unsupported + alias :visit_Float :unsupported + alias :visit_Hash :unsupported + alias :visit_NilClass :unsupported + alias :visit_String :unsupported + alias :visit_Symbol :unsupported + alias :visit_Time :unsupported + alias :visit_TrueClass :unsupported + + def visit_Arel_Nodes_InfixOperation(o, collector) + collector = visit o.left, collector + collector << " #{o.operator} " + visit o.right, collector + end + + alias :visit_Arel_Nodes_Addition :visit_Arel_Nodes_InfixOperation + alias :visit_Arel_Nodes_Subtraction :visit_Arel_Nodes_InfixOperation + alias :visit_Arel_Nodes_Multiplication :visit_Arel_Nodes_InfixOperation + alias :visit_Arel_Nodes_Division :visit_Arel_Nodes_InfixOperation + + def visit_Arel_Nodes_UnaryOperation(o, collector) + collector << " #{o.operator} " + visit o.expr, collector + end + + def visit_Array(o, collector) + inject_join o, collector, ", " + end + alias :visit_Set :visit_Array + + def quote(value) + return value if Arel::Nodes::SqlLiteral === value + @connection.quote value + end + + def quote_table_name(name) + return name if Arel::Nodes::SqlLiteral === name + @connection.quote_table_name(name) + end + + def quote_column_name(name) + return name if Arel::Nodes::SqlLiteral === name + @connection.quote_column_name(name) + end + + def maybe_visit(thing, collector) + return collector unless thing + collector << " " + visit thing, collector + end + + def inject_join(list, collector, join_str) + len = list.length - 1 + list.each_with_index.inject(collector) { |c, (x, i)| + if i == len + visit x, c + else + visit(x, c) << join_str + end + } + end + + def infix_value(o, collector, value) + collector = visit o.left, collector + collector << value + visit o.right, collector + end + + def aggregate(name, o, collector) + collector << "#{name}(" + if o.distinct + collector << "DISTINCT " + end + collector = inject_join(o.expressions, collector, ", ") << ")" + if o.alias + collector << " AS " + visit o.alias, collector + else + collector + end + end + end + end +end diff --git a/activerecord/lib/arel/visitors/visitor.rb b/activerecord/lib/arel/visitors/visitor.rb new file mode 100644 index 0000000000..1c17184e86 --- /dev/null +++ b/activerecord/lib/arel/visitors/visitor.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class Visitor + def initialize + @dispatch = get_dispatch_cache + end + + def accept(object, *args) + visit object, *args + end + + private + + attr_reader :dispatch + + def self.dispatch_cache + Hash.new do |hash, klass| + hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}" + end + end + + def get_dispatch_cache + self.class.dispatch_cache + end + + def visit(object, *args) + dispatch_method = dispatch[object.class] + send dispatch_method, object, *args + rescue NoMethodError => e + raise e if respond_to?(dispatch_method, true) + superklass = object.class.ancestors.find { |klass| + respond_to?(dispatch[klass], true) + } + raise(TypeError, "Cannot visit #{object.class}") unless superklass + dispatch[object.class] = dispatch[superklass] + retry + end + end + end +end diff --git a/activerecord/lib/arel/visitors/where_sql.rb b/activerecord/lib/arel/visitors/where_sql.rb new file mode 100644 index 0000000000..c6caf5e7c9 --- /dev/null +++ b/activerecord/lib/arel/visitors/where_sql.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Visitors + class WhereSql < Arel::Visitors::ToSql + def initialize(inner_visitor, *args, &block) + @inner_visitor = inner_visitor + super(*args, &block) + end + + private + + def visit_Arel_Nodes_SelectCore(o, collector) + collector << "WHERE " + wheres = o.wheres.map do |where| + Nodes::SqlLiteral.new(@inner_visitor.accept(where, collector.class.new).value) + end + + inject_join wheres, collector, " AND " + end + end + end +end diff --git a/activerecord/lib/arel/window_predications.rb b/activerecord/lib/arel/window_predications.rb new file mode 100644 index 0000000000..3a8ee41f8a --- /dev/null +++ b/activerecord/lib/arel/window_predications.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module WindowPredications + def over(expr = nil) + Nodes::Over.new(self, expr) + end + end +end diff --git a/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt index 60050e0bf8..60050e0bf8 100644 --- a/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb +++ b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 856fcc5897..a07b00ef79 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -15,12 +15,8 @@ module ActiveRecord migration_template @migration_template, File.join(db_migrate_path, "#{file_name}.rb") end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - attr_reader :migration_action, :join_tables - private + attr_reader :migration_action, :join_tables # Sets the default migration template that is being used for the generation of the migration. # Depending on command line arguments, the migration template and the table name instance diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt index 5f7201cfe1..5f7201cfe1 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt index 481c70201b..481c70201b 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt index 55dc65c8ad..55dc65c8ad 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt diff --git a/activerecord/lib/rails/generators/active_record/model/templates/module.rb b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt index a3bf1c37b6..a3bf1c37b6 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/module.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt |