diff options
Diffstat (limited to 'activerecord/lib')
126 files changed, 2390 insertions, 1509 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d43378c64f..7d66158f47 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -74,6 +74,7 @@ module ActiveRecord autoload :Translation autoload :Validations autoload :SecureToken + autoload :DatabaseSelector, "active_record/middleware/database_selector" eager_autoload do autoload :ActiveRecordError, "active_record/errors" @@ -153,6 +154,12 @@ module ActiveRecord end end + module Middleware + extend ActiveSupport::Autoload + + autoload :DatabaseSelector, "active_record/middleware/database_selector" + end + module Tasks extend ActiveSupport::Autoload diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index ab1e7ad269..7bdbd8ce69 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1293,8 +1293,9 @@ module ActiveRecord # # * <tt>:destroy</tt> causes all the associated objects to also be destroyed. # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed). - # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed. - # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records. + # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Polymorphic type will also be nullified + # on polymorphic associations. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there are any associated records. # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects. # # If using with the <tt>:through</tt> option, the association on the join model must be @@ -1436,8 +1437,9 @@ module ActiveRecord # # * <tt>:destroy</tt> causes the associated object to also be destroyed # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute) - # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. - # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record + # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Polymorphic type column is also nullified + # on polymorphic associations. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there is an associated record # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object # # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option. diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index bf4942aac8..0bb63b97ae 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -17,6 +17,23 @@ module ActiveRecord # CollectionAssociation # HasManyAssociation + ForeignAssociation # HasManyThroughAssociation + ThroughAssociation + # + # Associations in Active Record are middlemen between the object that + # holds the association, known as the <tt>owner</tt>, and the associated + # result set, known as the <tt>target</tt>. Association metadata is available in + # <tt>reflection</tt>, which is an instance of <tt>ActiveRecord::Reflection::AssociationReflection</tt>. + # + # For example, given + # + # class Blog < ActiveRecord::Base + # has_many :posts + # end + # + # blog = Blog.first + # + # The association of <tt>blog.posts</tt> has the object +blog+ as its + # <tt>owner</tt>, the collection of its posts as <tt>target</tt>, and + # the <tt>reflection</tt> object represents a <tt>:has_many</tt> macro. class Association #:nodoc: attr_reader :owner, :target, :reflection @@ -179,6 +196,20 @@ module ActiveRecord end private + def find_target + scope = self.scope + return scope.to_a if skip_statement_cache?(scope) + + conn = klass.connection + sc = reflection.association_scope_cache(conn, owner) do |params| + as = AssociationScope.create { params.bind } + target_scope.merge!(as.scope(self)) + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + sc.execute(binds, conn) { |record| set_inverse_instance(record) } || [] + end + # The scope for this association. # # Note that the association_scope is merged into the target_scope only when the diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 0a90a6104a..9e38380611 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -26,7 +26,9 @@ module ActiveRecord chain = get_chain(reflection, association, scope.alias_tracker) scope.extending! reflection.extensions - add_constraints(scope, owner, chain) + scope = add_constraints(scope, owner, chain) + scope.limit!(1) unless reflection.collection? + scope end def self.get_bind_values(owner, chain) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 840d900bbc..c3d4eab562 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -109,9 +109,8 @@ module ActiveRecord end end - # Add +records+ to this association. Returns +self+ so method calls may - # be chained. Since << flattens its argument list and inserts each record, - # +push+ and +concat+ behave identically. + # Add +records+ to this association. Since +<<+ flattens its argument list + # and inserts each record, +push+ and +concat+ behave identically. def concat(*records) records = records.flatten if owner.new_record? @@ -233,7 +232,7 @@ 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? || @association_ids + if loaded? || @association_ids || reflection.has_cached_counter? size.zero? else target.empty? && !scope.exists? @@ -303,23 +302,6 @@ module ActiveRecord end private - - def find_target - scope = self.scope - return scope.to_a if skip_statement_cache?(scope) - - conn = klass.connection - sc = reflection.association_scope_cache(conn, owner) do |params| - as = AssociationScope.create { params.bind } - target_scope.merge!(as.scope(self)) - end - - binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute(binds, conn) do |record| - set_inverse_instance(record) - end - end - # We have some records loaded from the database (persisted) and some that are # in-memory (memory). The same record may be represented in the persisted array # and in the memory array. @@ -364,7 +346,6 @@ module ActiveRecord add_to_target(record) do result = insert_record(record, true, raise) { @_was_loaded = loaded? - @association_ids = nil } end raise ActiveRecord::Rollback unless result @@ -401,6 +382,7 @@ module ActiveRecord delete_records(existing_records, method) if existing_records.any? @target -= records + @association_ids = nil records.each { |record| callback(:after_remove, record) } end @@ -413,9 +395,9 @@ module ActiveRecord end def replace_records(new_target, original_target) - delete(target - new_target) + delete(difference(target, new_target)) - unless concat(new_target - target) + unless concat(difference(new_target, target)) @target = original_target raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ "new records could not be saved." @@ -425,7 +407,7 @@ module ActiveRecord end def replace_common_records_in_memory(new_target, original_target) - common_records = new_target & original_target + common_records = intersection(new_target, original_target) common_records.each do |record| skip_callbacks = true replace_on_target(record, @target.index(record), skip_callbacks) @@ -441,7 +423,6 @@ module ActiveRecord unless owner.new_record? result &&= insert_record(record, true, raise) { @_was_loaded = loaded? - @association_ids = nil } end end @@ -464,6 +445,7 @@ module ActiveRecord if index target[index] = record elsif @_was_loaded || !loaded? + @association_ids = nil target << record end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 811fbfd927..edcb44f0fc 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -2,11 +2,8 @@ module ActiveRecord module Associations - # Association proxies in Active Record are middlemen between the object that - # holds the association, known as the <tt>@owner</tt>, and the actual associated - # object, known as the <tt>@target</tt>. The kind of association any proxy is - # about is available in <tt>@reflection</tt>. That's an instance of the class - # ActiveRecord::Reflection::AssociationReflection. + # Collection proxies in Active Record are middlemen between an + # <tt>association</tt>, and its <tt>target</tt> result set. # # For example, given # @@ -16,14 +13,14 @@ module ActiveRecord # # blog = Blog.first # - # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as - # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and - # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro. + # The collection proxy returned by <tt>blog.posts</tt> is built from a + # <tt>:has_many</tt> <tt>association</tt>, and delegates to a collection + # of posts as the <tt>target</tt>. # - # This class delegates unknown methods to <tt>@target</tt> via - # <tt>method_missing</tt>. + # This class delegates unknown methods to the <tt>association</tt>'s + # relation class via a delegate cache. # - # The <tt>@target</tt> object is not \loaded until needed. For example, + # The <tt>target</tt> result set is not loaded until needed. For example, # # blog.posts.count # @@ -366,34 +363,6 @@ module ActiveRecord @association.create!(attributes, &block) end - # Add one or more records to the collection by setting their foreign keys - # to the association's primary key. Since #<< flattens its argument list and - # inserts each record, +push+ and #concat behave identically. Returns +self+ - # so method calls may be chained. - # - # class Person < ActiveRecord::Base - # has_many :pets - # end - # - # person.pets.size # => 0 - # person.pets.concat(Pet.new(name: 'Fancy-Fancy')) - # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')) - # person.pets.size # => 3 - # - # person.id # => 1 - # person.pets - # # => [ - # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, - # # #<Pet id: 2, name: "Spook", person_id: 1>, - # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> - # # ] - # - # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')]) - # person.pets.size # => 5 - def concat(*records) - @association.concat(*records) - end - # Replaces this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. # @@ -500,7 +469,7 @@ module ActiveRecord # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3) def delete_all(dependent = nil) - @association.delete_all(dependent) + @association.delete_all(dependent).tap { reset_scope } end # Deletes the records of the collection directly from the database @@ -527,7 +496,7 @@ module ActiveRecord # # Pet.find(1) # => Couldn't find Pet with id=1 def destroy_all - @association.destroy_all + @association.destroy_all.tap { reset_scope } end # Deletes the +records+ supplied from the collection according to the strategy @@ -646,7 +615,7 @@ module ActiveRecord # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] def delete(*records) - @association.delete(*records) + @association.delete(*records).tap { reset_scope } end # Destroys the +records+ supplied and removes them from the collection. @@ -718,7 +687,7 @@ module ActiveRecord # # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6) def destroy(*records) - @association.destroy(*records) + @association.destroy(*records).tap { reset_scope } end ## @@ -1033,8 +1002,9 @@ module ActiveRecord end # Adds one or more +records+ to the collection by setting their foreign keys - # to the association's primary key. Returns +self+, so several appends may be - # chained together. + # to the association's primary key. Since +<<+ flattens its argument list and + # inserts each record, +push+ and +concat+ behave identically. Returns +self+ + # so several appends may be chained together. # # class Person < ActiveRecord::Base # has_many :pets @@ -1057,6 +1027,7 @@ module ActiveRecord end alias_method :push, :<< alias_method :append, :<< + alias_method :concat, :<< def prepend(*args) raise NoMethodError, "prepend on association is not defined. Please use <<, push or append" diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb index 40010cde03..59af6f54c3 100644 --- a/activerecord/lib/active_record/associations/foreign_association.rb +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -9,5 +9,12 @@ module ActiveRecord::Associations false end end + + def nullified_owner_attributes + Hash.new.tap do |attrs| + attrs[reflection.foreign_key] = nil + attrs[reflection.type] = nil if reflection.type.present? + end + end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index cf85a87fa7..5972846940 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -36,14 +36,6 @@ module ActiveRecord super end - def empty? - if reflection.has_cached_counter? - size.zero? - else - super - end - end - private # Returns the number of records in this collection. @@ -69,7 +61,7 @@ module ActiveRecord # If there's nothing in the database and @target has no new records # we are certain the current target is an empty array. This is a # documented side-effect of the method that may avoid an extra SELECT. - (@target ||= []) && loaded! if count == 0 + loaded! if count == 0 [association_scope.limit_value, count].compact.min end @@ -92,13 +84,14 @@ module ActiveRecord if method == :delete_all scope.delete_all else - scope.update_all(reflection.foreign_key => nil) + scope.update_all(nullified_owner_attributes) end end def delete_or_nullify_all_records(method) count = delete_count(method, scope) update_counter(-count) + count end # Deletes the records according to the <tt>:dependent</tt> option. @@ -130,6 +123,14 @@ module ActiveRecord end saved_successfully end + + def difference(a, b) + a - b + end + + def intersection(a, b) + a & b + end end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index f84ac65fa2..84a9797aa5 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -161,6 +161,30 @@ module ActiveRecord else update_counter(-count) end + + count + end + + def difference(a, b) + distribution = distribution(b) + + a.reject { |record| mark_occurrence(distribution, record) } + end + + def intersection(a, b) + distribution = distribution(b) + + a.select { |record| mark_occurrence(distribution, record) } + end + + def mark_occurrence(distribution, record) + distribution[record] > 0 && distribution[record] -= 1 + end + + def distribution(array) + array.each_with_object(Hash.new(0)) do |record, distribution| + distribution[record] += 1 + end end def through_records_for(record) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 390bfd8b08..99971286a3 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -33,7 +33,7 @@ module ActiveRecord target.destroy throw(:abort) unless target.destroyed? when :nullify - target.update_columns(reflection.foreign_key => nil) if target.persisted? + target.update_columns(nullified_owner_attributes) if target.persisted? end 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 4583d89cba..ca0305abbb 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_record/associations/join_dependency/join_part" +require "active_support/core_ext/array/extract" module ActiveRecord module Associations @@ -30,17 +31,21 @@ module ActiveRecord table = tables[-i] klass = reflection.klass - constraint = reflection.build_join_constraint(table, foreign_table) + join_scope = reflection.join_scope(table, foreign_table, foreign_klass) - joins << table.create_join(table, table.create_on(constraint), join_type) - - join_scope = reflection.join_scope(table, foreign_klass) arel = join_scope.arel(alias_tracker.aliases) + nodes = arel.constraints.first + + others = nodes.children.extract! do |node| + Arel.fetch_attribute(node) { |attr| attr.relation.name != table.name } + end + + joins << table.create_join(table, table.create_on(nodes), join_type) - if arel.constraints.any? + unless others.empty? joins.concat arel.join_sources right = joins.last.right - right.expr = right.expr.and(arel.constraints) + right.expr.children.concat(others) end # The current table in this iteration becomes the foreign table in the next diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index d6f7359055..7048ff43b8 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -42,11 +42,10 @@ module ActiveRecord def associate_records_to_owner(owner, records) association = owner.association(reflection.name) - association.loaded! if reflection.collection? - association.target.concat(records) + association.target = records else - association.target = records.first unless records.empty? + association.target = records.first end end @@ -116,7 +115,7 @@ module ActiveRecord def build_scope scope = klass.scope_for_association - if reflection.type + if reflection.type && !reflection.through_reflection? scope.where!(reflection.type => model.polymorphic_name) end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index a6b7ab80a2..32653956b2 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -7,14 +7,14 @@ module ActiveRecord def run(preloader) already_loaded = owners.first.association(through_reflection.name).loaded? through_scope = through_scope() - reflection_scope = target_reflection_scope through_preloaders = preloader.preload(owners, through_reflection.name, through_scope) middle_records = through_preloaders.flat_map(&:preloaded_records) - preloaders = preloader.preload(middle_records, source_reflection.name, reflection_scope) + preloaders = preloader.preload(middle_records, source_reflection.name, scope) @preloaded_records = preloaders.flat_map(&:preloaded_records) owners.each do |owner| through_records = Array(owner.association(through_reflection.name).target) + if already_loaded if source_type = reflection.options[:source_type] through_records = through_records.select do |record| @@ -24,19 +24,22 @@ module ActiveRecord else owner.association(through_reflection.name).reset if through_scope end + result = through_records.flat_map do |record| - association = record.association(source_reflection.name) - target = association.target - association.reset if preload_scope - target + record.association(source_reflection.name).target end + result.compact! - if reflection_scope - result.sort_by! { |rhs| preload_index[rhs] } if reflection_scope.order_values.any? - result.uniq! if reflection_scope.distinct_value - end + result.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any? + result.uniq! if scope.distinct_value associate_records_to_owner(owner, result) end + + unless scope.empty_scope? + middle_records.each do |owner| + owner.association(source_reflection.name).reset if owner + end + end end private @@ -91,16 +94,6 @@ module ActiveRecord scope unless scope.empty_scope? end - - def target_reflection_scope - if preload_scope - reflection_scope.merge(preload_scope) - elsif reflection.scope - reflection_scope - else - nil - end - end end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 8e50cce102..a92932fa4b 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -36,21 +36,7 @@ module ActiveRecord end def find_target - scope = self.scope - return scope.take if skip_statement_cache?(scope) - - conn = klass.connection - sc = reflection.association_scope_cache(conn, owner) do |params| - as = AssociationScope.create { params.bind } - target_scope.merge!(as.scope(self)).limit(1) - end - - binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute(binds, conn) do |record| - set_inverse_instance record - end.first - rescue ::RangeError - nil + super.first end def replace(record) diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index b6f0e18764..929045f29b 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -45,16 +45,14 @@ module ActiveRecord def execute_callstack_for_multiparameter_attributes(callstack) errors = [] callstack.each do |name, values_with_empty_parameters| - begin - if values_with_empty_parameters.each_value.all?(&:nil?) - values = nil - else - values = values_with_empty_parameters - end - send("#{name}=", values) - rescue => ex - errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) + if values_with_empty_parameters.each_value.all?(&:nil?) + values = nil + else + values = values_with_empty_parameters end + send("#{name}=", values) + rescue => ex + errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end unless errors.empty? error_descriptions = errors.map(&:message).join(",") diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index eeb88e4b7a..fd8c1da842 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -328,15 +328,8 @@ module ActiveRecord # person.attribute_for_inspect(:tag_ids) # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]" def attribute_for_inspect(attr_name) - value = read_attribute(attr_name) - - if value.is_a?(String) && value.length > 50 - "#{value[0, 50]}...".inspect - elsif value.is_a?(Date) || value.is_a?(Time) - %("#{value.to_s(:db)}") - else - value.inspect - end + value = _read_attribute(attr_name) + format_for_inspect(value) end # Returns +true+ if the specified +attribute+ has been set by the user or by a @@ -456,6 +449,16 @@ module ActiveRecord end end + def format_for_inspect(value) + if value.is_a?(String) && value.length > 50 + "#{value[0, 50]}...".inspect + elsif value.is_a?(Date) || value.is_a?(Time) + %("#{value.to_s(:db)}") + else + value.inspect + end + end + def readonly_attribute?(name) self.class.readonly_attributes.include?(name) end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index ebc2252c50..45e4b8adfa 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -158,9 +158,13 @@ module ActiveRecord end private - def write_attribute_without_type_cast(attr_name, _) - result = super - clear_attribute_change(attr_name) + def write_attribute_without_type_cast(attr_name, value) + name = attr_name.to_s + if self.class.attribute_alias?(name) + name = self.class.attribute_alias(name) + end + result = super(name, value) + clear_attribute_change(name) result end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 9b267bb7c0..6af5346fa7 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -14,38 +14,39 @@ module ActiveRecord [key] if key end - # Returns the primary key value. + # Returns the primary key column's value. def id sync_with_transaction_state primary_key = self.class.primary_key _read_attribute(primary_key) if primary_key end - # Sets the primary key value. + # Sets the primary key column's value. def id=(value) sync_with_transaction_state primary_key = self.class.primary_key _write_attribute(primary_key, value) if primary_key end - # Queries the primary key value. + # Queries the primary key column's value. def id? sync_with_transaction_state query_attribute(self.class.primary_key) end - # Returns the primary key value before type cast. + # Returns the primary key column's value before type cast. def id_before_type_cast sync_with_transaction_state read_attribute_before_type_cast(self.class.primary_key) end - # Returns the primary key previous value. + # Returns the primary key column's previous value. def id_was sync_with_transaction_state attribute_was(self.class.primary_key) end + # Returns the primary key column's value from the database. def id_in_database sync_with_transaction_state attribute_in_database(self.class.primary_key) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 6e1275e990..ffac5313ad 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -42,16 +42,8 @@ module ActiveRecord # This method exists to avoid the expensive primary_key check internally, without # breaking compatibility with the read_attribute API - if defined?(JRUBY_VERSION) - # This form is significantly faster on JRuby, and this is one of our biggest hotspots. - # https://github.com/jruby/jruby/pull/2562 - def _read_attribute(attr_name, &block) # :nodoc: - @attributes.fetch_value(attr_name.to_s, &block) - end - else - def _read_attribute(attr_name) # :nodoc: - @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? } - end + def _read_attribute(attr_name, &block) # :nodoc + @attributes.fetch_value(attr_name.to_s, &block) end alias :attribute :_read_attribute diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 1bffe89875..ef5444dfc3 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -95,7 +95,7 @@ module ActiveRecord # # private # def delete_parents - # self.class.delete_all "parent_id = #{id}" + # self.class.delete_by(parent_id: id) # end # end # @@ -324,7 +324,7 @@ module ActiveRecord private - def create_or_update(*) + def create_or_update(**) _run_save_callbacks { super } end @@ -332,7 +332,7 @@ module ActiveRecord _run_create_callbacks { super } end - def _update_record(*) + def _update_record _run_update_callbacks { super } end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 99934a0e31..0ded1a5318 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -185,7 +185,7 @@ module ActiveRecord def wait_poll(timeout) @num_waiting += 1 - t0 = Time.now + t0 = Concurrent.monotonic_time elapsed = 0 loop do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @@ -194,7 +194,7 @@ module ActiveRecord return remove if any? - elapsed = Time.now - t0 + elapsed = Concurrent.monotonic_time - t0 if elapsed >= timeout msg = "could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use" % [timeout, elapsed] @@ -686,13 +686,13 @@ module ActiveRecord end newly_checked_out = [] - timeout_time = Time.now + (@checkout_timeout * 2) + timeout_time = Concurrent.monotonic_time + (@checkout_timeout * 2) @available.with_a_bias_for(Thread.current) do loop do synchronize do return if collected_conns.size == @connections.size && @now_connecting == 0 - remaining_timeout = timeout_time - Time.now + remaining_timeout = timeout_time - Concurrent.monotonic_time remaining_timeout = 0 if remaining_timeout < 0 conn = checkout_for_exclusive_access(remaining_timeout) collected_conns << conn @@ -915,6 +915,16 @@ 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.create_owner_to_pool # :nodoc: + Concurrent::Map.new(initial_capacity: 2) do |h, k| + # Discard the parent's connection pools immediately; we have no need + # of them + discard_unowned_pools(h) + + h[k] = Concurrent::Map.new(initial_capacity: 2) + end + end + def self.unowned_pool_finalizer(pid_map) # :nodoc: lambda do |_| discard_unowned_pools(pid_map) @@ -929,13 +939,7 @@ module ActiveRecord 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 + @owner_to_pool = ConnectionHandler.create_owner_to_pool # Backup finalizer: if the forked child never needed a pool, the above # early discard has not occurred @@ -1006,7 +1010,16 @@ module ActiveRecord # for (not necessarily the current class). def retrieve_connection(spec_name) #:nodoc: pool = retrieve_connection_pool(spec_name) - raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool + + unless pool + # multiple database application + if ActiveRecord::Base.connection_handler != ActiveRecord::Base.default_connection_handler + raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found for the '#{ActiveRecord::Base.current_role}' role." + else + raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." + end + end + pool.connection end 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 0059f0b773..b5e6d03cf5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -20,9 +20,22 @@ module ActiveRecord raise "Passing bind parameters with an arel AST is forbidden. " \ "The values must be stored on the AST directly" end - sql, binds = visitor.compile(arel_or_sql_string.ast, collector) - [sql.freeze, binds || []] + + if prepared_statements + sql, binds = visitor.compile(arel_or_sql_string.ast, collector) + + if binds.length > bind_params_length + unprepared_statement do + sql, binds = to_sql_and_binds(arel_or_sql_string) + visitor.preparable = false + end + end + else + sql = visitor.compile(arel_or_sql_string.ast, collector) + end + [sql.freeze, binds] else + visitor.preparable = false if prepared_statements [arel_or_sql_string.dup.freeze, binds] end end @@ -47,13 +60,8 @@ module ActiveRecord arel = arel_from_relation(arel) sql, binds = to_sql_and_binds(arel, binds) - if !prepared_statements || (arel.is_a?(String) && preparable.nil?) - preparable = false - elsif binds.length > bind_params_length - sql, binds = unprepared_statement { to_sql_and_binds(arel) } - preparable = false - else - preparable = visitor.preparable + if preparable.nil? + preparable = prepared_statements ? visitor.preparable : false end if prepared_statements && preparable @@ -98,6 +106,11 @@ module ActiveRecord exec_query(sql, name).rows end + # Determines whether the SQL statement is a write query. + def write_query?(sql) + raise NotImplementedError + end + # Executes the SQL statement in the context of this connection and returns # the raw result from the connection adapter. # Note: depending on your database connector, the result returned by this @@ -118,7 +131,7 @@ module ActiveRecord # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) - sql, binds = sql_for_insert(sql, pk, nil, sequence_name, binds) + sql, binds = sql_for_insert(sql, pk, sequence_name, binds) exec_query(sql, name, binds) end @@ -168,13 +181,6 @@ module ActiveRecord exec_delete(sql, name, binds) end - # Returns +true+ when the connection adapter supports prepared statement - # caching, otherwise returns +false+ - def supports_statement_cache? # :nodoc: - true - end - deprecate :supports_statement_cache? - # Runs the given block in a database transaction, and returns the result # of the block. # @@ -331,7 +337,7 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). - # Most of adapters should implement `insert_fixtures` that leverages bulk SQL insert. + # Most of adapters should implement `insert_fixtures_set` that leverages bulk SQL insert. # We keep this method to provide fallback # for databases like sqlite that do not support bulk inserts. def insert_fixture(fixture, table_name) @@ -360,18 +366,6 @@ module ActiveRecord execute manager.to_sql, "Fixture Insert" end - # 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? - - execute(build_fixture_sql(fixtures, table_name), "Fixtures Insert") - end - def insert_fixtures_set(fixture_set, tables_to_delete = []) fixture_inserts = fixture_set.map do |table_name, fixtures| next if fixtures.empty? @@ -459,7 +453,7 @@ module ActiveRecord exec_query(sql, name, binds, prepare: true) end - def sql_for_insert(sql, pk, id_value, sequence_name, binds) + def sql_for_insert(sql, pk, sequence_name, binds) [sql, binds] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 8aeb934ec2..93b1c4e632 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -17,7 +17,7 @@ module ActiveRecord method_names.each do |method_name| base.class_eval <<-end_code, __FILE__, __LINE__ + 1 def #{method_name}(*) - clear_query_cache if @query_cache_enabled + ActiveRecord::Base.clear_query_caches_for_current_thread if @query_cache_enabled super end end_code @@ -96,6 +96,11 @@ module ActiveRecord if @query_cache_enabled && !locked?(arel) arel = arel_from_relation(arel) sql, binds = to_sql_and_binds(arel, binds) + + if preparable.nil? + preparable = prepared_statements ? visitor.preparable : false + end + cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) } else super diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 07e86afe9a..eefe621feb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -157,13 +157,9 @@ module ActiveRecord end end - def types_which_need_no_typecasting - [nil, Numeric, String] - end - def _quote(value) case value - when String, ActiveSupport::Multibyte::Chars + when String, Symbol, ActiveSupport::Multibyte::Chars "'#{quote_string(value.to_s)}'" when true then quoted_true when false then quoted_false @@ -174,7 +170,6 @@ module ActiveRecord when Type::Binary::Data then quoted_binary(value) when Type::Time::Value then "'#{quoted_time(value)}'" when Date, Time then "'#{quoted_date(value)}'" - when Symbol then "'#{quote_string(value.to_s)}'" when Class then "'#{value}'" else raise TypeError, "can't quote #{value.class.name}" end @@ -188,10 +183,9 @@ module ActiveRecord when false then unquoted_false # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s("F") + when nil, Numeric, String then value when Type::Time::Value then quoted_time(value) when Date, Time then quoted_date(value) - when *types_which_need_no_typecasting - value else raise TypeError end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 9d9e8a4110..7d20825a75 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -15,7 +15,7 @@ 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, + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn, private: true private @@ -39,7 +39,9 @@ module ActiveRecord end def visit_TableDefinition(o) - create_sql = +"CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} " + create_sql = +"CREATE#{table_modifier_in_create(o)} TABLE " + create_sql << "IF NOT EXISTS " if o.if_not_exists + create_sql << "#{quote_table_name(o.name)} " statements = o.columns.map { |c| accept c } statements << accept(o.primary_keys) if o.primary_keys @@ -48,7 +50,7 @@ module ActiveRecord statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) end - if supports_foreign_keys_in_create? + if supports_foreign_keys? statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) }) end @@ -119,7 +121,15 @@ module ActiveRecord sql end + # Returns any SQL string to go between CREATE and TABLE. May be nil. + def table_modifier_in_create(o) + " TEMPORARY" if o.temporary + end + def foreign_key_in_create(from_table, to_table, options) + prefix = ActiveRecord::Base.table_name_prefix + suffix = ActiveRecord::Base.table_name_suffix + to_table = "#{prefix}#{to_table}#{suffix}" options = foreign_key_options(from_table, to_table, options) accept ForeignKeyDefinition.new(from_table, to_table, options) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 70607fda5a..11d4b4a503 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -102,7 +102,7 @@ module ActiveRecord alias validated? validate? def export_name_on_schema_dump? - name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern + !ActiveRecord::SchemaDumper.fk_ignore_pattern.match?(name) if name end def defined_for?(to_table_ord = nil, to_table: nil, **options) @@ -198,41 +198,44 @@ module ActiveRecord end module ColumnMethods + extend ActiveSupport::Concern + # Appends a primary key definition to the table definition. # Can be called multiple times, but this is probably not a good idea. def primary_key(name, type = :primary_key, **options) column(name, type, options.merge(primary_key: true)) end + ## + # :method: column + # :call-seq: column(name, type, **options) + # # Appends a column or columns of a specified type. # # t.string(:goat) # t.string(:goat, :sheep) # # See TableDefinition#column - [ - :bigint, - :binary, - :boolean, - :date, - :datetime, - :decimal, - :float, - :integer, - :json, - :string, - :text, - :time, - :timestamp, - :virtual, - ].each do |column_type| - module_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{column_type}(*args, **options) - args.each { |name| column(name, :#{column_type}, options) } + + included do + define_column_methods :bigint, :binary, :boolean, :date, :datetime, :decimal, + :float, :integer, :json, :string, :text, :time, :timestamp, :virtual + + alias :numeric :decimal + end + + class_methods do + private def define_column_methods(*column_types) # :nodoc: + column_types.each do |column_type| + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{column_type}(*names, **options) + raise ArgumentError, "Missing column name(s) for #{column_type}" if names.empty? + names.each { |name| column(name, :#{column_type}, options) } + end + RUBY end - CODE + end end - alias_method :numeric, :decimal end # Represents the schema of an SQL table in an abstract way. This class @@ -256,15 +259,25 @@ module ActiveRecord class TableDefinition include ColumnMethods - attr_accessor :indexes - attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment + attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys - def initialize(name, temporary = false, options = nil, as = nil, comment: nil) + def initialize( + conn, + name, + temporary: false, + if_not_exists: false, + options: nil, + as: nil, + comment: nil, + ** + ) + @conn = conn @columns_hash = {} @indexes = [] @foreign_keys = [] @primary_keys = nil @temporary = temporary + @if_not_exists = if_not_exists @options = options @as = as @name = name @@ -351,7 +364,7 @@ module ActiveRecord # t.references :tagger, polymorphic: true # t.references :taggable, polymorphic: { default: 'Photo' }, index: false # end - def column(name, type, options = {}) + def column(name, type, **options) name = name.to_s type = type.to_sym if type options = options.dup @@ -385,10 +398,7 @@ module ActiveRecord end def foreign_key(table_name, options = {}) # :nodoc: - table_name_prefix = ActiveRecord::Base.table_name_prefix - table_name_suffix = ActiveRecord::Base.table_name_suffix - table_name = "#{table_name_prefix}#{table_name}#{table_name_suffix}" - foreign_keys.push([table_name, options]) + foreign_keys << [table_name, options] end # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and @@ -398,6 +408,10 @@ module ActiveRecord def timestamps(**options) options[:null] = false if options[:null].nil? + if !options.key?(:precision) && @conn.supports_datetime_with_precision? + options[:precision] = 6 + end + column(:created_at, :datetime, options) column(:updated_at, :datetime, options) end @@ -505,6 +519,7 @@ module ActiveRecord # t.json # t.virtual # t.remove + # t.remove_foreign_key # t.remove_references # t.remove_belongs_to # t.remove_index @@ -526,7 +541,7 @@ module ActiveRecord # t.column(:name, :string) # # See TableDefinition#column for details of the options you can use. - def column(column_name, type, options = {}) + def column(column_name, type, **options) index_options = options.delete(:index) @base.add_column(name, column_name, type, options) index(column_name, index_options.is_a?(Hash) ? index_options : {}) if index_options @@ -668,15 +683,26 @@ module ActiveRecord end alias :remove_belongs_to :remove_references - # Adds a foreign key. + # Adds a foreign key to the table using a supplied table name. # # t.foreign_key(:authors) + # t.foreign_key(:authors, column: :author_id, primary_key: "id") # # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key] def foreign_key(*args) @base.add_foreign_key(name, *args) end + # Removes the given foreign key from the table. + # + # t.remove_foreign_key(:authors) + # t.remove_foreign_key(column: :author_id) + # + # See {connection.remove_foreign_key}[rdoc-ref:SchemaStatements#remove_foreign_key] + def remove_foreign_key(*args) + @base.remove_foreign_key(name, *args) + end + # Checks to see if a foreign key exists. # # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors) 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 8a7020a799..b2cc60f363 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -2,6 +2,7 @@ require "active_record/migration/join_table" require "active_support/core_ext/string/access" +require "active_support/deprecation" require "digest/sha2" module ActiveRecord @@ -129,11 +130,11 @@ module ActiveRecord # column_exists?(:suppliers, :name, :string, null: false) # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) # - def column_exists?(table_name, column_name, type = nil, options = {}) + def column_exists?(table_name, column_name, type = nil, **options) column_name = column_name.to_s checks = [] checks << lambda { |c| c.name == column_name } - checks << lambda { |c| c.type == type } if type + checks << lambda { |c| c.type == type.to_sym rescue nil } if type column_options_keys.each do |attr| checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr) end @@ -205,6 +206,9 @@ module ActiveRecord # Set to true to drop the table before creating it. # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. + # [<tt>:if_not_exists</tt>] + # Set to true to avoid raising an error when the table already exists. + # Defaults to false. # [<tt>:as</tt>] # SQL to use to generate the table. When this option is used, the block is # ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options. @@ -287,8 +291,8 @@ module ActiveRecord # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id # # See also TableDefinition#column for details on how to create columns. - def create_table(table_name, comment: nil, **options) - td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment + def create_table(table_name, **options) + td = create_table_definition(table_name, options) if options[:id] != false && !options[:as] pk = options.fetch(:primary_key) do @@ -317,7 +321,9 @@ module ActiveRecord end if supports_comments? && !supports_comments_in_create? - change_table_comment(table_name, comment) if comment.present? + if table_comment = options[:comment].presence + change_table_comment(table_name, table_comment) + end td.columns.each do |column| change_column_comment(table_name, column.name, column.comment) if column.comment.present? @@ -578,7 +584,7 @@ module ActiveRecord # # Defines a column with a database-specific type. # add_column(:shapes, :triangle, 'polygon') # # ALTER TABLE "shapes" ADD "triangle" polygon - def add_column(table_name, column_name, type, options = {}) + def add_column(table_name, column_name, type, **options) at = create_alter_table table_name at.add_column(column_name, type, options) execute schema_creation.accept at @@ -846,7 +852,7 @@ module ActiveRecord # [<tt>:null</tt>] # Whether the column allows nulls. Defaults to true. # - # ====== Create a user_id bigint column without a index + # ====== Create a user_id bigint column without an index # # add_reference(:products, :user, index: false) # @@ -1023,9 +1029,7 @@ module ActiveRecord end def foreign_key_column_for(table_name) # :nodoc: - prefix = Base.table_name_prefix - suffix = Base.table_name_suffix - name = table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s + name = strip_table_name_prefix_and_suffix(table_name) "#{name.singularize}_id" end @@ -1045,15 +1049,18 @@ module ActiveRecord { primary_key: true } end - def assume_migrated_upto_version(version, migrations_paths) - migrations_paths = Array(migrations_paths) + def assume_migrated_upto_version(version, migrations_paths = nil) + unless migrations_paths.nil? + ActiveSupport::Deprecation.warn(<<~MSG) + Passing migrations_paths to #assume_migrated_upto_version is deprecated and will be removed in Rails 6.1. + MSG + end + version = version.to_i sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) - migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i) - versions = migration_context.migration_files.map do |file| - migration_context.parse_migration_filename(file).first.to_i - end + migrated = migration_context.get_all_versions + versions = migration_context.migrations.map(&:version) unless migrated.include?(version) execute "INSERT INTO #{sm_table} (version) VALUES (#{quote(version)})" @@ -1120,6 +1127,10 @@ module ActiveRecord def add_timestamps(table_name, options = {}) options[:null] = false if options[:null].nil? + if !options.key?(:precision) && supports_datetime_with_precision? + options[:precision] = 6 + end + add_column table_name, :created_at, :datetime, options add_column table_name, :updated_at, :datetime, options end @@ -1281,7 +1292,7 @@ module ActiveRecord end def create_table_definition(*args) - TableDefinition.new(*args) + TableDefinition.new(self, *args) end def create_alter_table(name) @@ -1315,6 +1326,12 @@ module ActiveRecord { column: column_names } end + def strip_table_name_prefix_and_suffix(table_name) + prefix = Base.table_name_prefix + suffix = Base.table_name_suffix + table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s + end + def foreign_key_name(table_name, options) options.fetch(:name) do identifier = "#{table_name}_#{options.fetch(:column)}_fk" diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 564b226b39..c9e84e48cc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -40,24 +40,6 @@ module ActiveRecord committed? || rolledback? end - def set_state(state) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - The set_state method is deprecated and will be removed in - Rails 6.0. Please use rollback! or commit! to set transaction - state directly. - MSG - case state - when :rolledback - rollback! - when :committed - commit! - when nil - nullify! - else - raise ArgumentError, "Invalid transaction state: #{state}" - end - end - def rollback! @children.each { |c| c.rollback! } @state = :rolledback @@ -137,7 +119,7 @@ module ActiveRecord record.committed! else # if not running callbacks, only adds the record to the parent transaction - record.add_to_transaction + connection.add_transaction_record(record) end end ensure @@ -223,9 +205,12 @@ module ActiveRecord run_commit_callbacks: run_commit_callbacks) end - transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled? + if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && options[:_lazy] != false + @has_unmaterialized_transactions = true + else + transaction.materialize! + end @stack.push(transaction) - @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions? transaction end end @@ -283,26 +268,24 @@ module ActiveRecord def within_new_transaction(options = {}) @connection.lock.synchronize do - begin - transaction = begin_transaction options - yield - rescue Exception => error - if transaction + transaction = begin_transaction options + yield + rescue Exception => error + if transaction + rollback_transaction + after_failure_actions(transaction, error) + end + raise + ensure + if !error && transaction + if Thread.current.status == "aborting" rollback_transaction - after_failure_actions(transaction, error) - end - raise - ensure - unless error - if Thread.current.status == "aborting" - rollback_transaction if transaction - else - begin - commit_transaction if transaction - rescue Exception - rollback_transaction(transaction) unless transaction.state.completed? - raise - end + else + begin + commit_transaction + rescue Exception + rollback_transaction(transaction) unless transaction.state.completed? + raise end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 0fe868478c..f205d77ddb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -6,6 +6,7 @@ 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 "active_support/deprecation" require "arel/collectors/bind" require "arel/collectors/composite" require "arel/collectors/sql_string" @@ -76,8 +77,8 @@ module ActiveRecord SIMPLE_INT = /\A\d+\z/ - attr_accessor :visitor, :pool - attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock + attr_accessor :pool + attr_reader :schema_cache, :visitor, :owner, :logger, :lock, :prepared_statements, :prevent_writes alias :in_use? :owner set_callback :checkin, :after, :enable_lazy_transactions! @@ -100,6 +101,11 @@ module ActiveRecord end end + def self.build_read_query_regexp(*parts) # :nodoc: + parts = parts.map { |part| /\A[\(\s]*#{part}/i } + Regexp.union(*parts) + end + def initialize(connection, logger = nil, config = {}) # :nodoc: super() @@ -112,6 +118,7 @@ module ActiveRecord @idle_since = Concurrent.monotonic_time @schema_cache = SchemaCache.new self @quoted_column_names, @quoted_table_names = {}, {} + @prevent_writes = false @visitor = arel_visitor @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new @@ -133,6 +140,26 @@ module ActiveRecord @config[:replica] || false end + # Determines whether writes are currently being prevents. + # + # Returns true if the connection is a replica, or if +prevent_writes+ + # is set to true. + def preventing_writes? + replica? || prevent_writes + end + + # Prevent writing to the database regardless of role. + # + # In some cases you may want to prevent writes to the database + # even if you are on a database that can write. `while_preventing_writes` + # will prevent writes to the database for the duration of the block. + def while_preventing_writes + original, @prevent_writes = @prevent_writes, true + yield + ensure + @prevent_writes = original + end + def migrations_paths # :nodoc: @config[:migrations_paths] || Migrator.migrations_paths end @@ -308,12 +335,18 @@ module ActiveRecord def supports_foreign_keys_in_create? supports_foreign_keys? end + deprecate :supports_foreign_keys_in_create? # Does this adapter support views? def supports_views? false end + # Does this adapter support materialized views? + def supports_materialized_views? + false + end + # Does this adapter support datetime with precision? def supports_datetime_with_precision? false @@ -473,15 +506,21 @@ module ActiveRecord @connection end - def case_sensitive_comparison(table, attribute, column, value) # :nodoc: - table[attribute].eq(value) + def default_uniqueness_comparison(attribute, value) # :nodoc: + case_sensitive_comparison(attribute, value) + end + + def case_sensitive_comparison(attribute, value) # :nodoc: + attribute.eq(value) end - def case_insensitive_comparison(table, attribute, column, value) # :nodoc: + def case_insensitive_comparison(attribute, value) # :nodoc: + column = column_for_attribute(attribute) + if can_perform_case_insensitive_comparison_for?(column) - table[attribute].lower.eq(table.lower(value)) + attribute.lower.eq(attribute.relation.lower(value)) else - table[attribute].eq(value) + attribute.eq(value) end end @@ -580,14 +619,12 @@ module ActiveRecord $1.to_i if sql_type =~ /\((.*)\)/ end - def translate_exception_class(e, sql) - begin - message = "#{e.class.name}: #{e.message}: #{sql}" - rescue Encoding::CompatibilityError - message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}" - end + def translate_exception_class(e, sql, binds) + message = "#{e.class.name}: #{e.message}" - exception = translate_exception(e, message) + exception = translate_exception( + e, message: message, sql: sql, binds: binds + ) exception.set_backtrace e.backtrace exception end @@ -600,24 +637,23 @@ module ActiveRecord binds: binds, type_casted_binds: type_casted_binds, statement_name: statement_name, - connection_id: object_id) do - begin - @lock.synchronize do - yield - end - rescue => e - raise translate_exception_class(e, sql) + connection_id: object_id, + connection: self) do + @lock.synchronize do + yield end + rescue => e + raise translate_exception_class(e, sql, binds) end end - def translate_exception(exception, message) + def translate_exception(exception, message:, sql:, binds:) # override in derived class case exception when RuntimeError exception else - ActiveRecord::StatementInvalid.new(message) + ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds) end end @@ -631,6 +667,11 @@ module ActiveRecord raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}") end + def column_for_attribute(attribute) + table_name = attribute.relation.name + schema_cache.columns_hash(table_name)[attribute.name.to_s] + end + def collector if prepared_statements Arel::Collectors::Composite.new( 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 cb5eeb64dd..37506a97e2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -29,7 +29,7 @@ module ActiveRecord NATIVE_DATABASE_TYPES = { primary_key: "bigint auto_increment PRIMARY KEY", string: { name: "varchar", limit: 255 }, - text: { name: "text", limit: 65535 }, + text: { name: "text" }, integer: { name: "int", limit: 4 }, float: { name: "float", limit: 24 }, decimal: { name: "decimal" }, @@ -37,7 +37,8 @@ module ActiveRecord timestamp: { name: "timestamp" }, time: { name: "time" }, date: { name: "date" }, - binary: { name: "blob", limit: 65535 }, + binary: { name: "blob" }, + blob: { name: "blob" }, boolean: { name: "tinyint", limit: 1 }, json: { name: "json" }, } @@ -72,6 +73,10 @@ module ActiveRecord !mariadb? && version >= "8.0.1" end + def supports_expression_index? + !mariadb? && version >= "8.0.13" + end + def supports_transaction_isolation? true end @@ -93,33 +98,17 @@ module ActiveRecord end def supports_datetime_with_precision? - if mariadb? - version >= "5.3.0" - else - version >= "5.6.4" - end + mariadb? || version >= "5.6.4" end def supports_virtual_columns? - if mariadb? - version >= "5.2.0" - else - version >= "5.7.5" - end + mariadb? || version >= "5.7.5" end def supports_advisory_locks? true end - def supports_longer_index_key_prefix? - if mariadb? - version >= "10.2.2" - else - version >= "5.7.9" - end - end - def get_advisory_lock(lock_name, timeout = 0) # :nodoc: query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1 end @@ -177,9 +166,9 @@ module ActiveRecord def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds)}" - start = Time.now + start = Concurrent.monotonic_time result = exec_query(sql, "EXPLAIN", binds) - elapsed = Time.now - start + elapsed = Concurrent.monotonic_time - start MySQL::ExplainPrettyPrinter.new.pp(result, elapsed) end @@ -246,7 +235,7 @@ module ActiveRecord execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}" elsif options[:charset] execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset])}" - elsif supports_longer_index_key_prefix? + elsif row_format_dynamic_by_default? execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET `utf8mb4`" else raise "Configure a supported :charset and ensure innodb_large_prefix is enabled to support indexes on varchar(255) string columns." @@ -442,30 +431,6 @@ module ActiveRecord table_options end - # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit: nil, precision: nil, scale: nil, unsigned: nil, **) # :nodoc: - sql = \ - case type.to_s - when "integer" - integer_to_sql(limit) - when "text" - text_to_sql(limit) - when "blob" - binary_to_sql(limit) - when "binary" - if (0..0xfff) === limit - "varbinary(#{limit})" - else - binary_to_sql(limit) - end - else - super - end - - sql = "#{sql} unsigned" if unsigned && type != :primary_key - sql - end - # SHOW VARIABLES LIKE 'name' def show_variable(name) query_value("SELECT @@#{name}", "SCHEMA") @@ -488,9 +453,11 @@ module ActiveRecord SQL end - def case_sensitive_comparison(table, attribute, column, value) # :nodoc: + def case_sensitive_comparison(attribute, value) # :nodoc: + column = column_for_attribute(attribute) + if column.collation && !column.case_sensitive? - table[attribute].eq(Arel::Nodes::Bin.new(value)) + attribute.eq(Arel::Nodes::Bin.new(value)) else super end @@ -591,13 +558,13 @@ module ActiveRecord m.alias_type %r(bit)i, "binary" m.register_type(%r(enum)i) do |sql_type| - limit = sql_type[/^enum\((.+)\)/i, 1] + limit = sql_type[/^enum\s*\((.+)\)/i, 1] .split(",").map { |enum| enum.strip.length - 2 }.max MysqlString.new(limit: limit) end m.register_type(%r(^set)i) do |sql_type| - limit = sql_type[/^set\((.+)\)/i, 1] + limit = sql_type[/^set\s*\((.+)\)/i, 1] .split(",").map { |set| set.strip.length - 1 }.sum - 1 MysqlString.new(limit: limit) end @@ -624,6 +591,8 @@ module ActiveRecord # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html ER_DUP_ENTRY = 1062 ER_NOT_NULL_VIOLATION = 1048 + ER_NO_REFERENCED_ROW = 1216 + ER_ROW_IS_REFERENCED = 1217 ER_DO_NOT_HAVE_DEFAULT = 1364 ER_ROW_IS_REFERENCED_2 = 1451 ER_NO_REFERENCED_ROW_2 = 1452 @@ -635,35 +604,36 @@ module ActiveRecord ER_LOCK_WAIT_TIMEOUT = 1205 ER_QUERY_INTERRUPTED = 1317 ER_QUERY_TIMEOUT = 3024 + ER_FK_INCOMPATIBLE_COLUMNS = 3780 - def translate_exception(exception, message) + def translate_exception(exception, message:, sql:, binds:) case error_number(exception) when ER_DUP_ENTRY - RecordNotUnique.new(message) - when ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 - InvalidForeignKey.new(message) - when ER_CANNOT_ADD_FOREIGN - mismatched_foreign_key(message) + RecordNotUnique.new(message, sql: sql, binds: binds) + when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 + InvalidForeignKey.new(message, sql: sql, binds: binds) + when ER_CANNOT_ADD_FOREIGN, ER_FK_INCOMPATIBLE_COLUMNS + mismatched_foreign_key(message, sql: sql, binds: binds) when ER_CANNOT_CREATE_TABLE if message.include?("errno: 150") - mismatched_foreign_key(message) + mismatched_foreign_key(message, sql: sql, binds: binds) else super end when ER_DATA_TOO_LONG - ValueTooLong.new(message) + ValueTooLong.new(message, sql: sql, binds: binds) when ER_OUT_OF_RANGE - RangeError.new(message) + RangeError.new(message, sql: sql, binds: binds) when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT - NotNullViolation.new(message) + NotNullViolation.new(message, sql: sql, binds: binds) when ER_LOCK_DEADLOCK - Deadlocked.new(message) + Deadlocked.new(message, sql: sql, binds: binds) when ER_LOCK_WAIT_TIMEOUT - LockWaitTimeout.new(message) + LockWaitTimeout.new(message, sql: sql, binds: binds) when ER_QUERY_TIMEOUT - StatementTimeout.new(message) + StatementTimeout.new(message, sql: sql, binds: binds) when ER_QUERY_INTERRUPTED - QueryCanceled.new(message) + QueryCanceled.new(message, sql: sql, binds: binds) else super end @@ -716,6 +686,12 @@ module ActiveRecord end def add_timestamps_for_alter(table_name, options = {}) + options[:null] = false if options[:null].nil? + + if !options.key?(:precision) && supports_datetime_with_precision? + options[:precision] = 6 + end + [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)] end @@ -794,47 +770,28 @@ module ActiveRecord Arel::Visitors::MySQL.new(self) end - def mismatched_foreign_key(message) - parts = message.scan(/`(\w+)`[ $)]/).flatten - MismatchedForeignKey.new( - self, - message: message, - table: parts[0], - foreign_key: parts[1], - target_table: parts[2], - primary_key: parts[3], - ) - end + def mismatched_foreign_key(message, sql:, binds:) + match = %r/ + (?:CREATE|ALTER)\s+TABLE\s*(?:`?\w+`?\.)?`?(?<table>\w+)`?.+? + FOREIGN\s+KEY\s*\(`?(?<foreign_key>\w+)`?\)\s* + REFERENCES\s*(`?(?<target_table>\w+)`?)\s*\(`?(?<primary_key>\w+)`?\) + /xmi.match(sql) - def integer_to_sql(limit) # :nodoc: - case limit - when 1; "tinyint" - when 2; "smallint" - when 3; "mediumint" - when nil, 4; "int" - when 5..8; "bigint" - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead.") - end - end + options = { + message: message, + sql: sql, + binds: binds, + } - def text_to_sql(limit) # :nodoc: - case limit - when 0..0xff; "tinytext" - when nil, 0x100..0xffff; "text" - when 0x10000..0xffffff; "mediumtext" - when 0x1000000..0xffffffff; "longtext" - else raise(ActiveRecordError, "No text type has byte length #{limit}") + if match + options[:table] = match[:table] + options[:foreign_key] = match[:foreign_key] + options[:target_table] = match[:target_table] + options[:primary_key] = match[:primary_key] + options[:primary_key_column] = column_for(match[:target_table], match[:primary_key]) end - end - def binary_to_sql(limit) # :nodoc: - case limit - when 0..0xff; "tinyblob" - when nil, 0x100..0xffff; "blob" - when 0x10000..0xffffff; "mediumblob" - when 0x1000000..0xffffffff; "longblob" - else raise(ActiveRecordError, "No binary type has byte length #{limit}") - end + MismatchedForeignKey.new(options) end def version_string diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 2e7a78215a..9eaf9d9a89 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -174,12 +174,12 @@ module ActiveRecord if e.path == path_to_adapter # We can assume that a non-builtin adapter was specified, so it's # either misspelled or missing from Gemfile. - raise e.class, "Could not load the '#{spec[:adapter]}' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace + raise LoadError, "Could not load the '#{spec[:adapter]}' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace # Bubbled up from the adapter require. Prefix the exception message # with some guidance about how to address it and reraise. else - raise e.class, "Error loading the '#{spec[:adapter]}' Active Record adapter. Missing a gem it depends on? #{e.message}", e.backtrace + raise LoadError, "Error loading the '#{spec[:adapter]}' Active Record adapter. Missing a gem it depends on? #{e.message}", e.backtrace end end @@ -248,10 +248,29 @@ module ActiveRecord if db_config resolve_connection(db_config.config).merge("name" => pool_name.to_s) else - raise(AdapterNotSpecified, "'#{env_name}' database is not configured. Available: #{configurations.configurations.map(&:env_name).join(", ")}") + raise AdapterNotSpecified, <<~MSG + The `#{env_name}` database is not configured for the `#{ActiveRecord::ConnectionHandling::DEFAULT_ENV.call}` environment. + + Available databases configurations are: + + #{build_configuration_sentence} + MSG end end + def build_configuration_sentence # :nodoc: + configs = configurations.configs_for(include_replicas: true) + + configs.group_by(&:env_name).map do |env, config| + namespaces = config.map(&:spec_name) + if namespaces.size > 1 + "#{env}: #{namespaces.join(", ")}" + else + env + end + end.join("\n") + end + # Accepts a hash. Expands the "url" key that contains a # URL database connection to a full connection # hash and merges with the rest of the hash. diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb index 3dcb916d99..1df4dea2d8 100644 --- a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb @@ -3,14 +3,19 @@ module ActiveRecord module ConnectionAdapters module DetermineIfPreparableVisitor - attr_reader :preparable + attr_accessor :preparable def accept(*) @preparable = true super end - def visit_Arel_Nodes_In(*) + def visit_Arel_Nodes_In(o, collector) + @preparable = false + super + end + + def visit_Arel_Nodes_NotIn(o, collector) @preparable = false super end 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 684c7042a7..6adcc14545 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -19,8 +19,19 @@ module ActiveRecord execute(sql, name).to_a end + READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback) # :nodoc: + private_constant :READ_QUERY + + def write_query?(sql) # :nodoc: + !READ_QUERY.match?(sql) + end + # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) + if preventing_writes? && write_query?(sql) + raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}" + end + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been # made since we established the connection @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone @@ -29,22 +40,26 @@ module ActiveRecord end def exec_query(sql, name = "SQL", binds = [], prepare: false) - materialize_transactions - if without_prepared_statement?(binds) execute_and_free(sql, name) do |result| - ActiveRecord::Result.new(result.fields, result.to_a) if result + if result + ActiveRecord::Result.new(result.fields, result.to_a) + else + ActiveRecord::Result.new([], []) + end end else exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result| - ActiveRecord::Result.new(result.fields, result.to_a) if result + if result + ActiveRecord::Result.new(result.fields, result.to_a) + else + ActiveRecord::Result.new([], []) + end end end end def exec_delete(sql, name = nil, binds = []) - materialize_transactions - if without_prepared_statement?(binds) execute_and_free(sql, name) { @connection.affected_rows } else @@ -103,6 +118,12 @@ module ActiveRecord end def exec_stmt_and_free(sql, name, binds, cache_stmt: false) + if preventing_writes? && write_query?(sql) + raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}" + end + + materialize_transactions + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been # made since we established the connection @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone 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 2ed4ad16ae..d21535a709 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -4,48 +4,56 @@ module ActiveRecord module ConnectionAdapters module MySQL module ColumnMethods - def blob(*args, **options) - args.each { |name| column(name, :blob, options) } - end + extend ActiveSupport::Concern - def tinyblob(*args, **options) - args.each { |name| column(name, :tinyblob, options) } - end + ## + # :method: blob + # :call-seq: blob(*names, **options) - def mediumblob(*args, **options) - args.each { |name| column(name, :mediumblob, options) } - end + ## + # :method: tinyblob + # :call-seq: tinyblob(*names, **options) - def longblob(*args, **options) - args.each { |name| column(name, :longblob, options) } - end + ## + # :method: mediumblob + # :call-seq: mediumblob(*names, **options) - def tinytext(*args, **options) - args.each { |name| column(name, :tinytext, options) } - end + ## + # :method: longblob + # :call-seq: longblob(*names, **options) - def mediumtext(*args, **options) - args.each { |name| column(name, :mediumtext, options) } - end + ## + # :method: tinytext + # :call-seq: tinytext(*names, **options) - def longtext(*args, **options) - args.each { |name| column(name, :longtext, options) } - end + ## + # :method: mediumtext + # :call-seq: mediumtext(*names, **options) - def unsigned_integer(*args, **options) - args.each { |name| column(name, :unsigned_integer, options) } - end + ## + # :method: longtext + # :call-seq: longtext(*names, **options) - def unsigned_bigint(*args, **options) - args.each { |name| column(name, :unsigned_bigint, options) } - end + ## + # :method: unsigned_integer + # :call-seq: unsigned_integer(*names, **options) - def unsigned_float(*args, **options) - args.each { |name| column(name, :unsigned_float, options) } - end + ## + # :method: unsigned_bigint + # :call-seq: unsigned_bigint(*names, **options) + + ## + # :method: unsigned_float + # :call-seq: unsigned_float(*names, **options) + + ## + # :method: unsigned_decimal + # :call-seq: unsigned_decimal(*names, **options) - def unsigned_decimal(*args, **options) - args.each { |name| column(name, :unsigned_decimal, options) } + included do + define_column_methods :blob, :tinyblob, :mediumblob, :longblob, + :tinytext, :mediumtext, :longtext, :unsigned_integer, :unsigned_bigint, + :unsigned_float, :unsigned_decimal end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index d23178e43c..57518b02fa 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -10,6 +10,10 @@ module ActiveRecord spec[:unsigned] = "true" if column.unsigned? spec[:auto_increment] = "true" if column.auto_increment? + if /\A(?<size>tiny|medium|long)(?:text|blob)/ =~ column.sql_type + spec = { size: size.to_sym.inspect }.merge!(spec) + end + if @connection.supports_virtual_columns? && column.virtual? spec[:as] = extract_expression_for_virtual_column(column) spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra) @@ -37,13 +41,15 @@ module ActiveRecord case column.sql_type when /\Atimestamp\b/ :timestamp - when "tinyblob" - :blob else super end end + def schema_limit(column) + super unless /\A(?:tiny|medium|long)?(?:text|blob)/.match?(column.sql_type) + end + def schema_precision(column) super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0 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 e167c01802..4018f0815c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -35,13 +35,39 @@ module ActiveRecord ] end - 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" + if row[:Expression] + expression = row[:Expression] + expression = +"(#{expression})" unless expression.start_with?("(") + indexes.last[-2] << expression + indexes.last[-1][:expressions] ||= {} + indexes.last[-1][:expressions][expression] = expression + indexes.last[-1][:orders][expression] = :desc if row[:Collation] == "D" + else + 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][row[:Column_name]] = :desc if row[:Collation] == "D" + end end end - indexes.map { |index| IndexDefinition.new(*index) } + indexes.map do |index| + options = index.last + + if expressions = options.delete(:expressions) + orders = options.delete(:orders) + lengths = options.delete(:lengths) + + columns = index[-2].map { |name| + [ name.to_sym, expressions[name] || +quote_column_name(name) ] + }.to_h + + index[-2] = add_options_for_index_columns( + columns, order: orders, length: lengths + ).values.join(", ") + end + + IndexDefinition.new(*index) + end end def remove_column(table_name, column_name, type = nil, options = {}) @@ -51,9 +77,13 @@ module ActiveRecord super end + def create_table(table_name, options: default_row_format, **) + super + end + def internal_string_options_for_primary_key super.tap do |options| - if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) && (mariadb? || version < "8.0.0") + if !row_format_dynamic_by_default? && CHARSETS_OF_4BYTES_MAXLEN.include?(charset) options[:collation] = collation.sub(/\A[^_]+/, "utf8") end end @@ -67,23 +97,72 @@ module ActiveRecord MySQL::SchemaDumper.create(self, options) end + # Maps logical Rails types to MySQL-specific data types. + def type_to_sql(type, limit: nil, precision: nil, scale: nil, size: limit_to_size(limit, type), unsigned: nil, **) + sql = + case type.to_s + when "integer" + integer_to_sql(limit) + when "text" + type_with_size_to_sql("text", size) + when "blob" + type_with_size_to_sql("blob", size) + when "binary" + if (0..0xfff) === limit + "varbinary(#{limit})" + else + type_with_size_to_sql("blob", size) + end + else + super + end + + sql = "#{sql} unsigned" if unsigned && type != :primary_key + sql + end + private CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"] + def row_format_dynamic_by_default? + if mariadb? + version >= "10.2.2" + else + version >= "5.7.9" + end + end + + def default_row_format + return if row_format_dynamic_by_default? + + unless defined?(@default_row_format) + if query_value("SELECT @@innodb_file_per_table = 1 AND @@innodb_file_format = 'Barracuda'") == 1 + @default_row_format = "ROW_FORMAT=DYNAMIC" + else + @default_row_format = nil + end + end + + @default_row_format + end + def schema_creation MySQL::SchemaCreation.new(self) end def create_table_definition(*args) - MySQL::TableDefinition.new(*args) + MySQL::TableDefinition.new(self, *args) end def new_column_from_field(table_name, field) type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - 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 + default, default_function = field[:Default], nil + + if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(default) + default, default_function = nil, default + elsif type_metadata.extra == "DEFAULT_GENERATED" + default = +"(#{default})" unless default.start_with?("(") + default, default_function = nil, default end MySQL::Column.new( @@ -142,6 +221,40 @@ module ActiveRecord schema, name = nil, schema unless name [schema, name] end + + def type_with_size_to_sql(type, size) + case size&.to_s + when nil, "tiny", "medium", "long" + "#{size}#{type}" + else + raise ArgumentError, + "#{size.inspect} is invalid :size value. Only :tiny, :medium, and :long are allowed." + end + end + + def limit_to_size(limit, type) + case type.to_s + when "text", "blob", "binary" + case limit + when 0..0xff; "tiny" + when nil, 0x100..0xffff; nil + when 0x10000..0xffffff; "medium" + when 0x1000000..0xffffffff; "long" + else raise ActiveRecordError, "No #{type} type has byte size #{limit}" + end + end + end + + def integer_to_sql(limit) + case limit + when 1; "tinyint" + when 2; "smallint" + when 3; "mediumint" + when nil, 4; "int" + when 5..8; "bigint" + else raise ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead." + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 6bd6b67165..41633872e2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -67,11 +67,22 @@ module ActiveRecord end end + READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback) # :nodoc: + private_constant :READ_QUERY + + def write_query?(sql) # :nodoc: + !READ_QUERY.match?(sql) + end + # Executes an SQL statement, returning a PG::Result object on success # or raising a PG::Error exception otherwise. # Note: the PG::Result object is manually memory managed; if you don't # need it specifically, you may want consider the <tt>exec_query</tt> wrapper. def execute(sql, name = nil) + if preventing_writes? && write_query?(sql) + raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}" + end + materialize_transactions log(sql, name) do @@ -99,7 +110,7 @@ module ActiveRecord end alias :exec_update :exec_delete - def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc: + def sql_for_insert(sql, pk, sequence_name, binds) # :nodoc: if pk.nil? # Extract the table from the insert sql. Yuck. table_ref = extract_table_ref_from_insert_sql(sql) 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 6fbeaa2b9e..b1dfbde86e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -5,7 +5,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Array < Type::Value # :nodoc: - include Type::Helpers::Mutable + include ActiveModel::Type::Helpers::Mutable Data = Struct.new(:encoder, :values) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index aabe83b85d..7b42677101 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -5,7 +5,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Hstore < Type::Value # :nodoc: - include Type::Helpers::Mutable + include ActiveModel::Type::Helpers::Mutable def type :hstore diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb index 7b057a8452..7f6adc351c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb @@ -5,7 +5,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class LegacyPoint < Type::Value # :nodoc: - include Type::Helpers::Mutable + include ActiveModel::Type::Helpers::Mutable def type :point diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index 02a9c506f6..8c74cecc4d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -7,7 +7,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Point < Type::Value # :nodoc: - include Type::Helpers::Mutable + include ActiveModel::Type::Helpers::Mutable def type :point 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 d85f9ab3ef..aa7701e038 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -64,7 +64,7 @@ module ActiveRecord end def type_cast_single_for_database(value) - infinity?(value) ? value : @subtype.serialize(value) + infinity?(value) ? value : @subtype.serialize(@subtype.cast(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 83c21ba6ea..203087bc36 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 @@ -36,7 +36,7 @@ module ActiveRecord def query_conditions_for_initial_load known_type_names = @store.keys.map { |n| "'#{n}'" } known_type_types = %w('r' 'e' 'd') - <<-SQL % [known_type_names.join(", "), known_type_types.join(", ")] + <<~SQL % [known_type_names.join(", "), known_type_types.join(", ")] WHERE t.typname IN (%s) OR t.typtype IN (%s) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index bc9b8dbfcf..28abdbd073 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -13,9 +13,12 @@ module ActiveRecord :uuid end - def cast(value) - value.to_s[ACCEPTABLE_UUID, 0] - end + private + + def cast_value(value) + casted = value.to_s + casted if casted.match?(ACCEPTABLE_UUID) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index e75202b0be..d40e0ef1f0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -93,11 +93,11 @@ module ActiveRecord elsif value.hex? "X'#{value}'" end - when Float - if value.infinite? || value.nan? - "'#{value}'" - else + when Numeric + if value.finite? super + else + "'#{value}'" end when OID::Array::Data _quote(encode_array(value)) @@ -138,7 +138,7 @@ module ActiveRecord end def encode_range(range) - "[#{type_cast_range_value(range.first)},#{type_cast_range_value(range.last)}#{range.exclude_end? ? ')' : ']'}" + "[#{type_cast_range_value(range.begin)},#{type_cast_range_value(range.end)}#{range.exclude_end? ? ')' : ']'}" end def determine_encoding_of_strings_in_array(value) 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 8e381a92cf..84dd28907b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb @@ -17,12 +17,59 @@ module ActiveRecord "VALIDATE CONSTRAINT #{quote_column_name(name)}" end + def visit_ChangeColumnDefinition(o) + column = o.column + column.sql_type = type_to_sql(column.type, column.options) + quoted_column_name = quote_column_name(o.name) + + change_column_sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{column.sql_type}" + + options = column_options(column) + + if options[:collation] + change_column_sql << " COLLATE \"#{options[:collation]}\"" + end + + if options[:using] + change_column_sql << " USING #{options[:using]}" + elsif options[:cast_as] + cast_as_type = type_to_sql(options[:cast_as], options) + change_column_sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" + end + + if options.key?(:default) + if options[:default].nil? + change_column_sql << ", ALTER COLUMN #{quoted_column_name} DROP DEFAULT" + else + quoted_default = quote_default_expression(options[:default], column) + change_column_sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quoted_default}" + end + end + + if options.key?(:null) + change_column_sql << ", ALTER COLUMN #{quoted_column_name} #{options[:null] ? 'DROP' : 'SET'} NOT NULL" + end + + change_column_sql + end + def add_column_options!(sql, options) if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" end super end + + # Returns any SQL string to go between CREATE and TABLE. May be nil. + def table_modifier_in_create(o) + # A table cannot be both TEMPORARY and UNLOGGED, since all TEMPORARY + # tables are already UNLOGGED. + if o.temporary + " TEMPORARY" + elsif o.unlogged + " UNLOGGED" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 206b855a18..3bb7c52899 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -4,6 +4,8 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module ColumnMethods + extend ActiveSupport::Concern + # Defines the primary key field. # Use of the native PostgreSQL UUID type is supported, and can be used # by defining your tables as such: @@ -51,130 +53,144 @@ module ActiveRecord super end - def bigserial(*args, **options) - args.each { |name| column(name, :bigserial, options) } - end + ## + # :method: bigserial + # :call-seq: bigserial(*names, **options) - def bit(*args, **options) - args.each { |name| column(name, :bit, options) } - end + ## + # :method: bit + # :call-seq: bit(*names, **options) - def bit_varying(*args, **options) - args.each { |name| column(name, :bit_varying, options) } - end + ## + # :method: bit_varying + # :call-seq: bit_varying(*names, **options) - def cidr(*args, **options) - args.each { |name| column(name, :cidr, options) } - end + ## + # :method: cidr + # :call-seq: cidr(*names, **options) - def citext(*args, **options) - args.each { |name| column(name, :citext, options) } - end + ## + # :method: citext + # :call-seq: citext(*names, **options) - def daterange(*args, **options) - args.each { |name| column(name, :daterange, options) } - end + ## + # :method: daterange + # :call-seq: daterange(*names, **options) - def hstore(*args, **options) - args.each { |name| column(name, :hstore, options) } - end + ## + # :method: hstore + # :call-seq: hstore(*names, **options) - def inet(*args, **options) - args.each { |name| column(name, :inet, options) } - end + ## + # :method: inet + # :call-seq: inet(*names, **options) - def interval(*args, **options) - args.each { |name| column(name, :interval, options) } - end + ## + # :method: interval + # :call-seq: interval(*names, **options) - def int4range(*args, **options) - args.each { |name| column(name, :int4range, options) } - end + ## + # :method: int4range + # :call-seq: int4range(*names, **options) - def int8range(*args, **options) - args.each { |name| column(name, :int8range, options) } - end + ## + # :method: int8range + # :call-seq: int8range(*names, **options) - def jsonb(*args, **options) - args.each { |name| column(name, :jsonb, options) } - end + ## + # :method: jsonb + # :call-seq: jsonb(*names, **options) - def ltree(*args, **options) - args.each { |name| column(name, :ltree, options) } - end + ## + # :method: ltree + # :call-seq: ltree(*names, **options) - def macaddr(*args, **options) - args.each { |name| column(name, :macaddr, options) } - end + ## + # :method: macaddr + # :call-seq: macaddr(*names, **options) - def money(*args, **options) - args.each { |name| column(name, :money, options) } - end + ## + # :method: money + # :call-seq: money(*names, **options) - def numrange(*args, **options) - args.each { |name| column(name, :numrange, options) } - end + ## + # :method: numrange + # :call-seq: numrange(*names, **options) - def oid(*args, **options) - args.each { |name| column(name, :oid, options) } - end + ## + # :method: oid + # :call-seq: oid(*names, **options) - def point(*args, **options) - args.each { |name| column(name, :point, options) } - end + ## + # :method: point + # :call-seq: point(*names, **options) - def line(*args, **options) - args.each { |name| column(name, :line, options) } - end + ## + # :method: line + # :call-seq: line(*names, **options) - def lseg(*args, **options) - args.each { |name| column(name, :lseg, options) } - end + ## + # :method: lseg + # :call-seq: lseg(*names, **options) - def box(*args, **options) - args.each { |name| column(name, :box, options) } - end + ## + # :method: box + # :call-seq: box(*names, **options) - def path(*args, **options) - args.each { |name| column(name, :path, options) } - end + ## + # :method: path + # :call-seq: path(*names, **options) - def polygon(*args, **options) - args.each { |name| column(name, :polygon, options) } - end + ## + # :method: polygon + # :call-seq: polygon(*names, **options) - def circle(*args, **options) - args.each { |name| column(name, :circle, options) } - end + ## + # :method: circle + # :call-seq: circle(*names, **options) - def serial(*args, **options) - args.each { |name| column(name, :serial, options) } - end + ## + # :method: serial + # :call-seq: serial(*names, **options) - def tsrange(*args, **options) - args.each { |name| column(name, :tsrange, options) } - end + ## + # :method: tsrange + # :call-seq: tsrange(*names, **options) - def tstzrange(*args, **options) - args.each { |name| column(name, :tstzrange, options) } - end + ## + # :method: tstzrange + # :call-seq: tstzrange(*names, **options) - def tsvector(*args, **options) - args.each { |name| column(name, :tsvector, options) } - end + ## + # :method: tsvector + # :call-seq: tsvector(*names, **options) - def uuid(*args, **options) - args.each { |name| column(name, :uuid, options) } - end + ## + # :method: uuid + # :call-seq: uuid(*names, **options) + + ## + # :method: xml + # :call-seq: xml(*names, **options) - def xml(*args, **options) - args.each { |name| column(name, :xml, options) } + included do + define_column_methods :bigserial, :bit, :bit_varying, :cidr, :citext, :daterange, + :hstore, :inet, :interval, :int4range, :int8range, :jsonb, :ltree, :macaddr, + :money, :numrange, :oid, :point, :line, :lseg, :box, :path, :polygon, :circle, + :serial, :tsrange, :tstzrange, :tsvector, :uuid, :xml end end class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods + attr_reader :unlogged + + def initialize(*) + super + @unlogged = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables + end + private def integer_like_primary_key_type(type, options) if type == :bigint || options[:limit] == 8 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 fae3ddbad4..d694a4f47d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -22,8 +22,8 @@ module ActiveRecord def create_database(name, options = {}) options = { encoding: "utf8" }.merge!(options.symbolize_keys) - option_string = options.inject("") do |memo, (key, value)| - memo += case key + option_string = options.each_with_object(+"") do |(key, value), memo| + memo << case key when :owner " OWNER = \"#{value}\"" when :template @@ -68,7 +68,7 @@ module ActiveRecord table = quoted_scope(table_name) index = quoted_scope(index_name) - query_value(<<-SQL, "SCHEMA").to_i > 0 + query_value(<<~SQL, "SCHEMA").to_i > 0 SELECT COUNT(*) FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid @@ -85,7 +85,7 @@ module ActiveRecord def indexes(table_name) # :nodoc: scope = quoted_scope(table_name) - result = query(<<-SQL, "SCHEMA") + 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 FROM pg_class t @@ -124,7 +124,7 @@ module ActiveRecord # 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| + 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(" ") @@ -196,7 +196,7 @@ module ActiveRecord # Returns an array of schema names. def schema_names - query_values(<<-SQL, "SCHEMA") + query_values(<<~SQL, "SCHEMA") SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' @@ -302,7 +302,7 @@ module ActiveRecord def pk_and_sequence_for(table) #:nodoc: # First try looking for a sequence with a dependency on the # given table's primary key. - result = query(<<-end_sql, "SCHEMA")[0] + result = query(<<~SQL, "SCHEMA")[0] SELECT attr.attname, nsp.nspname, seq.relname FROM pg_class seq, pg_attribute attr, @@ -319,10 +319,10 @@ module ActiveRecord AND cons.contype = 'p' AND dep.classid = 'pg_class'::regclass AND dep.refobjid = #{quote(quote_table_name(table))}::regclass - end_sql + SQL if result.nil? || result.empty? - result = query(<<-end_sql, "SCHEMA")[0] + result = query(<<~SQL, "SCHEMA")[0] SELECT attr.attname, nsp.nspname, CASE WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL @@ -339,7 +339,7 @@ module ActiveRecord WHERE t.oid = #{quote(quote_table_name(table))}::regclass AND cons.contype = 'p' AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate' - end_sql + SQL end pk = result.shift @@ -548,14 +548,14 @@ module ActiveRecord # The hard limit is 1GB, because of a 32-bit size field, and TOAST. case limit when nil, 0..0x3fffffff; super(type) - else raise(ActiveRecordError, "No binary type has byte size #{limit}.") + else raise ActiveRecordError, "No binary type has byte size #{limit}. The limit on binary can be at most 1GB - 1byte." end when "text" # PostgreSQL doesn't support limits on text columns. # The hard limit is 1GB, according to section 8.3 in the manual. case limit when nil, 0..0x3fffffff; super(type) - else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.") + else raise ActiveRecordError, "No text type has byte size #{limit}. The limit on text can be at most 1GB - 1byte." end when "integer" case limit @@ -637,7 +637,7 @@ module ActiveRecord end def create_table_definition(*args) - PostgreSQL::TableDefinition.new(*args) + PostgreSQL::TableDefinition.new(self, *args) end def create_alter_table(name) @@ -683,38 +683,20 @@ 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}" - 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) + td = create_table_definition(table_name) + cd = td.new_column_definition(column_name, type, options) + sqls = [schema_creation.accept(ChangeColumnDefinition.new(cd, column_name))] 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: + def change_column_default_for_alter(table_name, column_name, default_or_changes) column = column_for(table_name, column_name) return unless column @@ -729,11 +711,17 @@ module ActiveRecord 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" + def change_column_null_for_alter(table_name, column_name, null, default = nil) + "ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL" end def add_timestamps_for_alter(table_name, options = {}) + options[:null] = false if options[:null].nil? + + if !options.key?(:precision) && supports_datetime_with_precision? + options[:precision] = 6 + end + [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)] end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb index bfd300723d..f2f4701500 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -68,7 +68,7 @@ module ActiveRecord # * <tt>"schema_name".table_name</tt> # * <tt>"schema.name"."table name"</tt> def extract_schema_qualified_name(string) - schema, table = string.scan(/[^".\s]+|"[^"]*"/) + schema, table = string.scan(/[^".]+|"[^"]*"/) if table.nil? table = schema schema = nil diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index a280ca500a..95beeb4cae 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -5,7 +5,7 @@ gem "pg", ">= 0.18", "< 2.0" require "pg" # Use async_exec instead of exec_params on pg versions before 1.1 -class ::PG::Connection +class ::PG::Connection # :nodoc: unless self.public_method_defined?(:async_exec_params) remove_method :exec_params alias exec_params async_exec @@ -85,6 +85,19 @@ module ActiveRecord class PostgreSQLAdapter < AbstractAdapter ADAPTER_NAME = "PostgreSQL" + ## + # :singleton-method: + # PostgreSQL allows the creation of "unlogged" tables, which do not record + # data in the PostgreSQL Write-Ahead Log. This can make the tables faster, + # but significantly increases the risk of data loss if the database + # crashes. As a result, this should not be used in production + # environments. If you would like all created tables to be unlogged in + # the test environment you can add the following line to your test.rb + # file: + # + # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + class_attribute :create_unlogged_tables, default: false + NATIVE_DATABASE_TYPES = { primary_key: "bigserial primary key", string: { name: "character varying" }, @@ -172,7 +185,7 @@ module ActiveRecord end def supports_json? - postgresql_version >= 90200 + true end def supports_comments? @@ -223,6 +236,8 @@ module ActiveRecord # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil + @default_timezone = nil + @timestamp_decoder = nil @max_identifier_length = nil configure_connection @@ -319,16 +334,16 @@ module ActiveRecord end def supports_ranges? - # Range datatypes weren't introduced until PostgreSQL 9.2 - postgresql_version >= 90200 + true end + deprecate :supports_ranges? def supports_materialized_views? - postgresql_version >= 90300 + true end def supports_foreign_tables? - postgresql_version >= 90300 + true end def supports_pgcrypto_uuid? @@ -412,8 +427,8 @@ module ActiveRecord private def check_version - if postgresql_version < 90100 - raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." + if postgresql_version < 90300 + raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.3." end end @@ -428,28 +443,28 @@ module ActiveRecord LOCK_NOT_AVAILABLE = "55P03" QUERY_CANCELED = "57014" - def translate_exception(exception, message) + def translate_exception(exception, message:, sql:, binds:) return exception unless exception.respond_to?(:result) case exception.result.try(:error_field, PG::PG_DIAG_SQLSTATE) when UNIQUE_VIOLATION - RecordNotUnique.new(message) + RecordNotUnique.new(message, sql: sql, binds: binds) when FOREIGN_KEY_VIOLATION - InvalidForeignKey.new(message) + InvalidForeignKey.new(message, sql: sql, binds: binds) when VALUE_LIMIT_VIOLATION - ValueTooLong.new(message) + ValueTooLong.new(message, sql: sql, binds: binds) when NUMERIC_VALUE_OUT_OF_RANGE - RangeError.new(message) + RangeError.new(message, sql: sql, binds: binds) when NOT_NULL_VIOLATION - NotNullViolation.new(message) + NotNullViolation.new(message, sql: sql, binds: binds) when SERIALIZATION_FAILURE - SerializationFailure.new(message) + SerializationFailure.new(message, sql: sql, binds: binds) when DEADLOCK_DETECTED - Deadlocked.new(message) + Deadlocked.new(message, sql: sql, binds: binds) when LOCK_NOT_AVAILABLE - LockWaitTimeout.new(message) + LockWaitTimeout.new(message, sql: sql, binds: binds) when QUERY_CANCELED - QueryCanceled.new(message) + QueryCanceled.new(message, sql: sql, binds: binds) else super end @@ -576,18 +591,11 @@ module ActiveRecord def load_additional_types(oids = nil) initializer = OID::TypeMapInitializer.new(type_map) - if supports_ranges? - query = <<-SQL - SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype - FROM pg_type as t - LEFT JOIN pg_range as r ON oid = rngtypid - SQL - else - query = <<-SQL - SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype - FROM pg_type as t - SQL - end + query = <<~SQL + SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype + FROM pg_type as t + LEFT JOIN pg_range as r ON oid = rngtypid + SQL if oids query += "WHERE t.oid::integer IN (%s)" % oids.join(", ") @@ -603,6 +611,10 @@ module ActiveRecord FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: def execute_and_clear(sql, name, binds, prepare: false) + if preventing_writes? && write_query?(sql) + raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}" + end + if without_prepared_statement?(binds) result = exec_no_cache(sql, name, []) elsif !prepare @@ -618,6 +630,10 @@ module ActiveRecord def exec_no_cache(sql, name, binds) materialize_transactions + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + update_typemap_for_default_timezone + type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @@ -628,8 +644,9 @@ module ActiveRecord def exec_cache(sql, name, binds) materialize_transactions + update_typemap_for_default_timezone - stmt_key = prepare_statement(sql) + stmt_key = prepare_statement(sql, binds) type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds, stmt_key) do @@ -683,7 +700,7 @@ module ActiveRecord # Prepare the statement if it hasn't been prepared, return # the statement key. - def prepare_statement(sql) + def prepare_statement(sql, binds) @lock.synchronize do sql_key = sql_key(sql) unless @statements.key? sql_key @@ -691,7 +708,7 @@ module ActiveRecord begin @connection.prepare nextkey, sql rescue => e - raise translate_exception_class(e, sql) + raise translate_exception_class(e, sql, binds) end # Clear the queue @connection.get_last_result @@ -763,7 +780,7 @@ module ActiveRecord # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) - query(<<-end_sql, "SCHEMA") + query(<<~SQL, "SCHEMA") SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, c.collname, col_description(a.attrelid, a.attnum) AS comment @@ -774,7 +791,7 @@ module ActiveRecord WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum - end_sql + SQL end def extract_table_ref_from_insert_sql(sql) @@ -789,7 +806,7 @@ module ActiveRecord def can_perform_case_insensitive_comparison_for?(column) @case_insensitive_cache ||= {} @case_insensitive_cache[column.sql_type] ||= begin - sql = <<-end_sql + sql = <<~SQL SELECT exists( SELECT * FROM pg_proc WHERE proname = 'lower' @@ -801,7 +818,7 @@ module ActiveRecord WHERE proname = 'lower' AND castsource = #{quote column.sql_type}::regtype ) - end_sql + SQL execute_and_clear(sql, "SCHEMA", []) do |result| result.getvalue(0, 0) end @@ -816,6 +833,18 @@ module ActiveRecord @connection.type_map_for_queries = map end + def update_typemap_for_default_timezone + if @default_timezone != ActiveRecord::Base.default_timezone && @timestamp_decoder + decoder_class = ActiveRecord::Base.default_timezone == :utc ? + PG::TextDecoder::TimestampUtc : + PG::TextDecoder::TimestampWithoutTimeZone + + @timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h) + @connection.type_map_for_results.add_coder(@timestamp_decoder) + @default_timezone = ActiveRecord::Base.default_timezone + end + end + def add_pg_decoders coders_by_name = { "int2" => PG::TextDecoder::Integer, @@ -826,8 +855,15 @@ module ActiveRecord "float8" => PG::TextDecoder::Float, "bool" => PG::TextDecoder::Boolean, } + + if defined?(PG::TextDecoder::TimestampUtc) + # Use native PG encoders available since pg-1.1 + coders_by_name["timestamp"] = PG::TextDecoder::TimestampUtc + coders_by_name["timestamptz"] = PG::TextDecoder::TimestampWithTimeZone + end + known_coder_types = coders_by_name.keys.map { |n| quote(n) } - query = <<-SQL % known_coder_types.join(", ") + query = <<~SQL % known_coder_types.join(", ") SELECT t.oid, t.typname FROM pg_type as t WHERE t.typname IN (%s) @@ -841,6 +877,10 @@ module ActiveRecord map = PG::TypeMapByOid.new coders.each { |coder| map.add_coder(coder) } @connection.type_map_for_results = map + + # extract timestamp decoder for use in update_typemap_for_default_timezone + @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" } + update_typemap_for_default_timezone end def construct_coder(row, coder_class) diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index c29cf1f9a1..c10765f42d 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -77,6 +77,11 @@ module ActiveRecord }] end + # Checks whether the columns hash is already cached for a table. + def columns_hash?(table_name) + @columns_hash.key?(table_name) + end + # Clears out internal caches def clear! @columns.clear diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index b2dcdb5373..cb9d32a577 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -12,6 +12,10 @@ module ActiveRecord quote_column_name(attr) end + def quote_table_name(name) + @quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze + end + def quote_column_name(name) @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}") end @@ -26,19 +30,19 @@ module ActiveRecord end def quoted_true - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "1" : "'t'" + "1" end def unquoted_true - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 1 : "t" + 1 end def quoted_false - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "0" : "'f'" + "0" end def unquoted_false - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 0 : "f" + 0 end private 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 48277f0ae2..bd649451d6 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -11,7 +11,7 @@ module ActiveRecord # See https://www.sqlite.org/fileformat2.html#intschema next if row["name"].starts_with?("sqlite_") - index_sql = query_value(<<-SQL, "SCHEMA") + index_sql = query_value(<<~SQL, "SCHEMA") SELECT sql FROM sqlite_master WHERE name = #{quote(row['name'])} AND type = 'index' @@ -52,6 +52,32 @@ module ActiveRecord end.compact end + def add_foreign_key(from_table, to_table, **options) + alter_table(from_table) do |definition| + to_table = strip_table_name_prefix_and_suffix(to_table) + definition.foreign_key(to_table, options) + end + end + + def remove_foreign_key(from_table, to_table = nil, **options) + to_table ||= options[:to_table] + options = options.except(:name, :to_table) + foreign_keys = foreign_keys(from_table) + + fkey = foreign_keys.detect do |fk| + table = to_table || begin + table = options[:column].to_s.delete_suffix("_id") + Base.pluralize_table_names ? table.pluralize : table + end + table = strip_table_name_prefix_and_suffix(table) + fk_to_table = strip_table_name_prefix_and_suffix(fk.to_table) + fk_to_table == table && options.all? { |k, v| fk.options[k].to_s == v.to_s } + end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table}") + + foreign_keys.delete(fkey) + alter_table(from_table, foreign_keys) + end + def create_schema_dumper(options) SQLite3::SchemaDumper.create(self, options) end @@ -62,7 +88,7 @@ module ActiveRecord end def create_table_definition(*args) - SQLite3::TableDefinition.new(*args) + SQLite3::TableDefinition.new(self, *args) end def new_column_from_field(table_name, field) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index e0355a316b..9f28fdf749 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -9,7 +9,7 @@ require "active_record/connection_adapters/sqlite3/schema_definitions" require "active_record/connection_adapters/sqlite3/schema_dumper" require "active_record/connection_adapters/sqlite3/schema_statements" -gem "sqlite3", "~> 1.3.6" +gem "sqlite3", "~> 1.3", ">= 1.3.6" require "sqlite3" module ActiveRecord @@ -76,22 +76,15 @@ module ActiveRecord json: { name: "json" }, } - ## - # :singleton-method: - # Indicates whether boolean values are stored in sqlite3 databases as 1 - # and 0 or 't' and 'f'. Leaving <tt>ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer</tt> - # set to false is deprecated. SQLite databases have used 't' and 'f' to - # serialize boolean values and must have old data converted to 1 and 0 - # (its native boolean serialization) before setting this flag to true. - # Conversion can be accomplished by setting up a rake task which runs - # - # ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1) - # ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0) - # for all models and all boolean columns, after which the flag must be set - # to true by adding the following to your <tt>application.rb</tt> file: - # - # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true - class_attribute :represent_boolean_as_integer, default: false + def self.represent_boolean_as_integer=(value) # :nodoc: + if value == false + raise "`.represent_boolean_as_integer=` is now always true, so make sure your application can work with it and remove this settings." + end + + ActiveSupport::Deprecation.warn( + "`.represent_boolean_as_integer=` is now always true, so setting this is deprecated and will be removed in Rails 6.1." + ) + end class StatementPool < ConnectionAdapters::StatementPool # :nodoc: private @@ -128,7 +121,7 @@ module ActiveRecord true end - def supports_foreign_keys_in_create? + def supports_foreign_keys? true end @@ -209,12 +202,23 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== #++ + READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :explain, :select, :pragma, :release, :savepoint, :rollback) # :nodoc: + private_constant :READ_QUERY + + def write_query?(sql) # :nodoc: + !READ_QUERY.match?(sql) + end + def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", [])) end def exec_query(sql, name = nil, binds = [], prepare: false) + if preventing_writes? && write_query?(sql) + raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}" + end + materialize_transactions type_casted_binds = type_casted_binds(binds) @@ -257,6 +261,10 @@ module ActiveRecord end def execute(sql, name = nil) #:nodoc: + if preventing_writes? && write_query?(sql) + raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}" + end + materialize_transactions log(sql, name) do @@ -299,11 +307,6 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end - 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 invalid_alter_table_type?(type, options) alter_table(table_name) do |definition| @@ -317,6 +320,9 @@ module ActiveRecord def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc: alter_table(table_name) do |definition| definition.remove_column column_name + definition.foreign_keys.delete_if do |_, fk_options| + fk_options[:column] == column_name.to_s + end end end @@ -375,14 +381,6 @@ module ActiveRecord end end - def insert_fixtures(rows, 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 @@ -396,6 +394,12 @@ module ActiveRecord end private + # See https://www.sqlite.org/limits.html, + # the default value is 999 when not configured. + def bind_params_length + 999 + end + def check_version if sqlite_version < "3.8.0" raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8." @@ -420,9 +424,8 @@ module ActiveRecord type.to_sym == :primary_key || options[:primary_key] end - def alter_table(table_name, options = {}) + def alter_table(table_name, foreign_keys = foreign_keys(table_name), **options) altered_table_name = "a#{table_name}" - foreign_keys = foreign_keys(table_name) caller = lambda do |definition| rename = options[:rename] || {} @@ -430,7 +433,8 @@ module ActiveRecord if column = rename[fk.options[:column]] fk.options[:column] = column end - definition.foreign_key(fk.to_table, fk.options) + to_table = strip_table_name_prefix_and_suffix(fk.to_table) + definition.foreign_key(to_table, fk.options) end yield definition if block_given? @@ -523,18 +527,18 @@ module ActiveRecord @sqlite_version ||= SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)")) end - def translate_exception(exception, message) + def translate_exception(exception, message:, sql:, binds:) case exception.message # SQLite 3.8.2 returns a newly formatted error message: # UNIQUE constraint failed: *table_name*.*column_name* # Older versions of SQLite return: # column *column_name* is not unique when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/ - RecordNotUnique.new(message) + RecordNotUnique.new(message, sql: sql, binds: binds) when /.* may not be NULL/, /NOT NULL constraint failed: .*/ - NotNullViolation.new(message) + NotNullViolation.new(message, sql: sql, binds: binds) when /FOREIGN KEY constraint failed/i - InvalidForeignKey.new(message) + InvalidForeignKey.new(message, sql: sql, binds: binds) else super end @@ -544,7 +548,7 @@ module ActiveRecord def table_structure_with_collation(table_name, basic_structure) collation_hash = {} - sql = <<-SQL + sql = <<~SQL SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 5141271db9..53069cd899 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -107,14 +107,21 @@ module ActiveRecord # end def connected_to(database: nil, role: nil, &blk) if database && role - raise ArgumentError, "connected_to can only accept a database or role argument, but not both arguments." + raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments." elsif database + if database.is_a?(Hash) + role, database = database.first + role = role.to_sym + else + role = database.to_sym + end + config_hash = resolve_config_for_connection(database) - handler = lookup_connection_handler(database.to_sym) + handler = lookup_connection_handler(role) - with_handler(database.to_sym) do + with_handler(role) do handler.establish_connection(config_hash) - return yield + yield end elsif role with_handler(role.to_sym, &blk) @@ -123,6 +130,29 @@ module ActiveRecord end end + # Returns true if role is the current connected role. + # + # ActiveRecord::Base.connected_to(role: :writing) do + # ActiveRecord::Base.connected_to?(role: :writing) #=> true + # ActiveRecord::Base.connected_to?(role: :reading) #=> false + # end + def connected_to?(role:) + current_role == role.to_sym + end + + # Returns the symbol representing the current connected role. + # + # ActiveRecord::Base.connected_to(role: :writing) do + # ActiveRecord::Base.current_role #=> :writing + # end + # + # ActiveRecord::Base.connected_to(role: :reading) do + # ActiveRecord::Base.current_role #=> :reading + # end + def current_role + connection_handlers.key(connection_handler) + end + def lookup_connection_handler(handler_key) # :nodoc: connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new end @@ -146,6 +176,15 @@ module ActiveRecord config_hash end + # Clears the query cache for all connections associated with the current thread. + def clear_query_caches_for_current_thread + ActiveRecord::Base.connection_handlers.each_value do |handler| + handler.connection_pool_list.each do |pool| + pool.connection.clear_query_cache if pool.active_connection? + end + end + end + # Returns the connection currently associated with the class. This can # also be used to "borrow" the connection to do database work unrelated # to any of the specific Active Records. diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 0718688863..18cfac1f2f 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -2,15 +2,13 @@ require "active_support/core_ext/hash/indifferent_access" require "active_support/core_ext/string/filters" +require "active_support/parameter_filter" require "concurrent/map" -require "set" module ActiveRecord module Core extend ActiveSupport::Concern - FILTERED = "[FILTERED]" # :nodoc: - included do ## # :singleton-method: @@ -103,6 +101,7 @@ module ActiveRecord # environment where dumping schema is rarely needed. mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true + mattr_accessor :database_selector, instance_writer: false ## # :singleton-method: # Specifies which database schemas to dump when calling db:structure:dump. @@ -126,20 +125,23 @@ module ActiveRecord mattr_accessor :connection_handlers, instance_accessor: false, default: {} + mattr_accessor :writing_role, instance_accessor: false, default: :writing + + mattr_accessor :reading_role, instance_accessor: false, default: :reading + class_attribute :default_connection_handler, instance_writer: false self.filter_attributes = [] def self.connection_handler - ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler + Thread.current.thread_variable_get("ar_connection_handler") || default_connection_handler end def self.connection_handler=(handler) - ActiveRecord::RuntimeRegistry.connection_handler = handler + Thread.current.thread_variable_set("ar_connection_handler", handler) end self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new - self.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } end module ClassMethods @@ -171,19 +173,17 @@ module ActiveRecord where(key => params.bind).limit(1) } - record = statement.execute([id], connection).first + record = statement.execute([id], connection)&.first unless record raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", name, primary_key, id) end record - rescue ::RangeError - raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'", - name, primary_key) end def find_by(*args) # :nodoc: - return super if scope_attributes? || reflect_on_all_aggregations.any? + return super if scope_attributes? || reflect_on_all_aggregations.any? || + columns_hash.key?(inheritance_column) && !base_class? hash = args.first @@ -203,11 +203,9 @@ module ActiveRecord where(wheres).limit(1) } begin - statement.execute(hash.values, connection).first + statement.execute(hash.values, connection)&.first rescue TypeError raise ActiveRecord::StatementInvalid - rescue ::RangeError - nil end end @@ -239,9 +237,7 @@ module ActiveRecord end # Specifies columns which shouldn't be exposed while calling +#inspect+. - def filter_attributes=(attributes_names) - @filter_attributes = attributes_names.map(&:to_s).to_set - end + attr_writer :filter_attributes # Returns a string like 'Post(id:integer, title:string, body:text)' def inspect # :nodoc: @@ -286,6 +282,10 @@ module ActiveRecord TypeCaster::Map.new(self) end + def _internal? # :nodoc: + false + end + private def cached_find_by_statement(key, &block) @@ -354,9 +354,7 @@ module ActiveRecord # Initialize an empty model object from +attributes+. # +attributes+ should be an attributes object, and unlike the # `initialize` method, no assignment calls are made per attribute. - # - # :nodoc: - def init_with_attributes(attributes, new_record = false) + def init_with_attributes(attributes, new_record = false) # :nodoc: init_internals @new_record = new_record @@ -480,6 +478,14 @@ module ActiveRecord end end + def present? # :nodoc: + true + end + + def blank? # :nodoc: + false + end + # Returns +true+ if the record is read only. Records loaded through joins with piggy-back # attributes will be marked as read only since they cannot be saved. def readonly? @@ -502,11 +508,14 @@ module ActiveRecord inspection = if defined?(@attributes) && @attributes self.class.attribute_names.collect do |name| if has_attribute?(name) - if filter_attribute?(name) - "#{name}: #{ActiveRecord::Core::FILTERED}" + attr = _read_attribute(name) + value = if attr.nil? + attr.inspect else - "#{name}: #{attribute_for_inspect(name)}" + attr = format_for_inspect(attr) + inspection_filter.filter_param(name, attr) end + "#{name}: #{value}" end end.compact.join(", ") else @@ -522,18 +531,16 @@ module ActiveRecord return super if custom_inspect_method_defined? pp.object_address_group(self) do if defined?(@attributes) && @attributes - column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } - pp.seplist(column_names, proc { pp.text "," }) do |column_name| + attr_names = self.class.attribute_names.select { |name| has_attribute?(name) } + pp.seplist(attr_names, proc { pp.text "," }) do |attr_name| pp.breakable " " pp.group(1) do - pp.text column_name + pp.text attr_name pp.text ":" pp.breakable - if filter_attribute?(column_name) - pp.text ActiveRecord::Core::FILTERED - else - pp.pp read_attribute(column_name) - end + value = _read_attribute(attr_name) + value = inspection_filter.filter_param(attr_name, value) unless value.nil? + pp.pp value end end else @@ -585,8 +592,14 @@ module ActiveRecord self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner end - def filter_attribute?(attribute_name) - self.class.filter_attributes.include?(attribute_name) && !read_attribute(attribute_name).nil? + def inspection_filter + @inspection_filter ||= begin + mask = DelegateClass(::String).new(ActiveSupport::ParameterFilter::FILTERED) + def mask.pretty_print(pp) + pp.text __getobj__ + end + ActiveSupport::ParameterFilter.new(self.class.filter_attributes, mask: mask) + end end end end diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index fa1589511e..a6c702cbbc 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -25,11 +25,11 @@ module ActiveRecord # # Options: # - # <tt>env_name:</tt> The environment name. Defaults to nil which will collect + # <tt>env_name:</tt> The environment name. Defaults to +nil+ which will collect # configs for all environments. - # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults + # <tt>spec_name:</tt> The specification name (i.e. primary, animals, etc.). Defaults # to +nil+. - # <tt>include_replicas:</tt> Determines whether to include replicas in the + # <tt>include_replicas:</tt> Determines whether to include replicas in # the returned list. Most of the time we're only iterating over the write # connection (i.e. migrations don't need to run for the write and read connection). # Defaults to +false+. @@ -102,6 +102,7 @@ module ActiveRecord def build_configs(configs) return configs.configurations if configs.is_a?(DatabaseConfigurations) + return configs if configs.is_a?(Array) build_db_config = configs.each_pair.flat_map do |env_name, config| walk_configs(env_name.to_s, "primary", config) @@ -124,21 +125,21 @@ module ActiveRecord end def build_db_config_from_string(env_name, spec_name, config) - begin - url = config - uri = URI.parse(url) - if uri.try(:scheme) - ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url) - end - rescue URI::InvalidURIError - ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) + url = config + uri = URI.parse(url) + if uri.try(:scheme) + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url) end + rescue URI::InvalidURIError + ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) end def build_db_config_from_hash(env_name, spec_name, config) - if url = config["url"] + if config.has_key?("url") + url = config["url"] config_without_url = config.dup config_without_url.delete "url" + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url) elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String }) ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) @@ -157,7 +158,7 @@ module ActiveRecord configs else configs.map do |config| - ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config) + ActiveRecord::DatabaseConfigurations::UrlConfig.new(config.env_name, config.spec_name, url, config.config) end end else @@ -166,21 +167,38 @@ module ActiveRecord end def method_missing(method, *args, &blk) - if Hash.method_defined?(method) - ActiveSupport::Deprecation.warn \ - "Returning a hash from ActiveRecord::Base.configurations is deprecated. Therefore calling `#{method}` on the hash is also deprecated. Please switch to using the `configs_for` method instead to collect and iterate over database configurations." - end - case method when :each, :first + throw_getter_deprecation(method) configurations.send(method, *args, &blk) when :fetch + throw_getter_deprecation(method) configs_for(env_name: args.first) when :values + throw_getter_deprecation(method) configurations.map(&:config) + when :[]= + throw_setter_deprecation(method) + + env_name = args[0] + config = args[1] + + remaining_configs = configurations.reject { |db_config| db_config.env_name == env_name } + new_config = build_configs(env_name => config) + new_configs = remaining_configs + new_config + + ActiveRecord::Base.configurations = new_configs else - super + raise NotImplementedError, "`ActiveRecord::Base.configurations` in Rails 6 now returns an object instead of a hash. The `#{method}` method is not supported. Please use `configs_for` or consult the documentation for supported methods." end end + + def throw_setter_deprecation(method) + ActiveSupport::Deprecation.warn("Setting `ActiveRecord::Base.configurations` with `#{method}` is deprecated. Use `ActiveRecord::Base.configurations=` directly to set the configurations instead.") + end + + def throw_getter_deprecation(method) + ActiveSupport::Deprecation.warn("`ActiveRecord::Base.configurations` no longer returns a hash. Methods that act on the hash like `#{method}` are deprecated and will be removed in Rails 6.1. Use the `configs_for` method to collect and iterate over the database configurations.") + end end end diff --git a/activerecord/lib/active_record/database_configurations/hash_config.rb b/activerecord/lib/active_record/database_configurations/hash_config.rb index c176a62458..574cb6bf15 100644 --- a/activerecord/lib/active_record/database_configurations/hash_config.rb +++ b/activerecord/lib/active_record/database_configurations/hash_config.rb @@ -16,7 +16,7 @@ module ActiveRecord # # Options are: # - # <tt>:env_name</tt> - The Rails environment, ie "development" + # <tt>:env_name</tt> - The Rails environment, i.e. "development" # <tt>:spec_name</tt> - The specification name. In a standard two-tier # database configuration this will default to "primary". In a multiple # database three-tier database configuration this corresponds to the name diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb index 81917fc4c1..8e8aa69478 100644 --- a/activerecord/lib/active_record/database_configurations/url_config.rb +++ b/activerecord/lib/active_record/database_configurations/url_config.rb @@ -56,12 +56,17 @@ module ActiveRecord end private - def build_config(original_config, url) - if /^jdbc:/.match?(url) - hash = { "url" => url } + + def build_url_hash(url) + if url.nil? || /^jdbc:/.match?(url) + { "url" => url } else - hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash + ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash end + end + + def build_config(original_config, url) + hash = build_url_hash(url) if original_config[env_name] original_config[env_name].merge(hash) diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 3d97e4e513..8077630aeb 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -31,7 +31,9 @@ module ActiveRecord # as well. With the above example: # # Conversation.active + # Conversation.not_active # Conversation.archived + # Conversation.not_archived # # Of course, you can also query them directly if the scopes don't fit your # needs: @@ -149,6 +151,7 @@ module ActiveRecord klass = self enum_prefix = definitions.delete(:_prefix) enum_suffix = definitions.delete(:_suffix) + enum_scopes = definitions.delete(:_scopes) definitions.each do |name, values| assert_valid_enum_definition_values(values) # statuses = { } @@ -157,7 +160,7 @@ module ActiveRecord # def self.statuses() statuses end detect_enum_conflict!(name, name.pluralize, true) - singleton_class.send(:define_method, name.pluralize) { enum_values } + singleton_class.define_method(name.pluralize) { enum_values } defined_enums[name] = enum_values detect_enum_conflict!(name, name) @@ -195,10 +198,17 @@ module ActiveRecord define_method("#{value_method_name}!") { update!(attr => value) } # scope :active, -> { where(status: 0) } - klass.send(:detect_enum_conflict!, name, value_method_name, true) - klass.scope value_method_name, -> { where(attr => value) } + # scope :not_active, -> { where.not(status: 0) } + if enum_scopes != false + klass.send(:detect_enum_conflict!, name, value_method_name, true) + klass.scope value_method_name, -> { where(attr => value) } + + klass.send(:detect_enum_conflict!, name, "not_#{value_method_name}", true) + klass.scope "not_#{value_method_name}", -> { where.not(attr => value) } + end end end + enum_values.freeze end end @@ -218,6 +228,10 @@ module ActiveRecord MSG raise ArgumentError, error_message end + + if values.is_a?(Hash) && values.keys.any?(&:blank?) || values.is_a?(Array) && values.any?(&:blank?) + raise ArgumentError, "Enum label name must not be blank." + end end ENUM_CONFLICT_MESSAGE = \ diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index f61bc7b9e8..48c0e8bbcc 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -49,6 +49,10 @@ module ActiveRecord class ConnectionNotEstablished < ActiveRecordError end + # Raised when a write to the database is attempted on a read only connection. + class ReadOnlyError < ActiveRecordError + end + # Raised when Active Record cannot find a record by given id or set of ids. class RecordNotFound < ActiveRecordError attr_reader :model, :primary_key, :id @@ -97,9 +101,13 @@ module ActiveRecord # # Wraps the underlying database error as +cause+. class StatementInvalid < ActiveRecordError - def initialize(message = nil) + def initialize(message = nil, sql: nil, binds: nil) super(message || $!.try(:message)) + @sql = sql + @binds = binds end + + attr_reader :sql, :binds end # Defunct wrapper class kept for compatibility. @@ -118,16 +126,26 @@ module ActiveRecord # Raised when a foreign key constraint cannot be added because the column type does not match the referenced column type. class MismatchedForeignKey < StatementInvalid - def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil) - @adapter = adapter + def initialize( + message: nil, + sql: nil, + binds: nil, + table: nil, + foreign_key: nil, + target_table: nil, + primary_key: nil, + primary_key_column: nil + ) if table - 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}`). + type = primary_key_column.bigint? ? :bigint : primary_key_column.type + msg = <<~EOM.squish + Column `#{foreign_key}` on table `#{table}` does not match column `#{primary_key}` on `#{target_table}`, + which has type `#{primary_key_column.sql_type}`. + To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :#{type}. + (For example `t.#{type} :#{foreign_key}`). EOM else - msg = +<<~EOM + msg = <<~EOM.squish 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 @@ -135,13 +153,8 @@ module ActiveRecord if message msg << "\nOriginal message: #{message}" end - super(msg) + super(msg, sql: sql, binds: binds) end - - private - def column_type(table, column) - @adapter.columns(table).detect { |c| c.name == column }.sql_type - end end # Raised when a record cannot be inserted or updated because it would violate a not null constraint. diff --git a/activerecord/lib/active_record/fixture_set/model_metadata.rb b/activerecord/lib/active_record/fixture_set/model_metadata.rb index edc03939c8..fb23df6f45 100644 --- a/activerecord/lib/active_record/fixture_set/model_metadata.rb +++ b/activerecord/lib/active_record/fixture_set/model_metadata.rb @@ -3,9 +3,8 @@ module ActiveRecord class FixtureSet class ModelMetadata # :nodoc: - def initialize(model_class, table_name) + def initialize(model_class) @model_class = model_class - @table_name = table_name end def primary_key_name @@ -23,18 +22,12 @@ module ActiveRecord def timestamp_column_names @timestamp_column_names ||= - %w(created_at created_on updated_at updated_on) & column_names + %w(created_at created_on updated_at updated_on) & @model_class.column_names end def inheritance_column_name @inheritance_column_name ||= @model_class && @model_class.inheritance_column end - - private - - def column_names - @column_names ||= @model_class.connection.columns(@table_name).collect(&:name) - end end end end diff --git a/activerecord/lib/active_record/fixture_set/table_row.rb b/activerecord/lib/active_record/fixture_set/table_row.rb index 5f72c1df38..cb4726f1ee 100644 --- a/activerecord/lib/active_record/fixture_set/table_row.rb +++ b/activerecord/lib/active_record/fixture_set/table_row.rb @@ -3,6 +3,38 @@ module ActiveRecord class FixtureSet class TableRow # :nodoc: + class ReflectionProxy # :nodoc: + def initialize(association) + @association = association + end + + def join_table + @association.join_table + end + + def name + @association.name + end + + def primary_key_type + @association.klass.type_for_attribute(@association.klass.primary_key).type + end + end + + class HasManyThroughProxy < ReflectionProxy # :nodoc: + def rhs_key + @association.foreign_key + end + + def lhs_key + @association.through_reflection.foreign_key + end + + def join_table + @association.through_reflection.table_name + end + end + def initialize(fixture, table_rows:, label:, now:) @table_rows = table_rows @label = label @@ -31,7 +63,7 @@ module ActiveRecord interpolate_label generate_primary_key resolve_enums - @table_rows.resolve_sti_reflections(@row) + resolve_sti_reflections end def reflection_class @@ -74,6 +106,48 @@ module ActiveRecord end end end + + def resolve_sti_reflections + # If STI is used, find the correct subclass for association reflection + reflection_class._reflections.each_value do |association| + case association.macro + when :belongs_to + # Do not replace association name with association foreign key if they are named the same + fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s + + if association.name.to_s != fk_name && value = @row.delete(association.name.to_s) + if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + # support polymorphic belongs_to as "label (Type)" + @row[association.foreign_type] = $1 + end + + fk_type = reflection_class.type_for_attribute(fk_name).type + @row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) + end + when :has_many + if association.options[:through] + add_join_records(HasManyThroughProxy.new(association)) + end + end + end + end + + def add_join_records(association) + # This is the case when the join table has no fixtures file + if (targets = @row.delete(association.name.to_s)) + table_name = association.join_table + column_type = association.primary_key_type + lhs_key = association.lhs_key + rhs_key = association.rhs_key + + targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) + joins = targets.map do |target| + { lhs_key => @row[model_metadata.primary_key_name], + rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } + end + @table_rows.tables[table_name].concat(joins) + end + end end end end diff --git a/activerecord/lib/active_record/fixture_set/table_rows.rb b/activerecord/lib/active_record/fixture_set/table_rows.rb index e8335a2e10..23814b6cb5 100644 --- a/activerecord/lib/active_record/fixture_set/table_rows.rb +++ b/activerecord/lib/active_record/fixture_set/table_rows.rb @@ -6,88 +6,31 @@ require "active_record/fixture_set/model_metadata" module ActiveRecord class FixtureSet class TableRows # :nodoc: - class ReflectionProxy # :nodoc: - def initialize(association) - @association = association - end - - def join_table - @association.join_table - end - - def name - @association.name - end - - def primary_key_type - @association.klass.type_for_attribute(@association.klass.primary_key).type - end - end - - class HasManyThroughProxy < ReflectionProxy # :nodoc: - def rhs_key - @association.foreign_key - end - - def lhs_key - @association.through_reflection.foreign_key - end - - def join_table - @association.through_reflection.table_name - end - end - def initialize(table_name, model_class:, fixtures:, config:) - @table_name = table_name @model_class = model_class # track any join tables we need to insert later @tables = Hash.new { |h, table| h[table] = [] } - build_table_rows_from(fixtures, config) + # ensure this table is loaded before any HABTM associations + @tables[table_name] = nil + + build_table_rows_from(table_name, fixtures, config) end - attr_reader :table_name, :model_class + attr_reader :tables, :model_class def to_hash @tables.transform_values { |rows| rows.map(&:to_hash) } end def model_metadata - @model_metadata ||= ModelMetadata.new(model_class, table_name) - end - - def resolve_sti_reflections(row) - # If STI is used, find the correct subclass for association reflection - reflection_class = reflection_class_for(row) - - reflection_class._reflections.each_value do |association| - case association.macro - when :belongs_to - # Do not replace association name with association foreign key if they are named the same - fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s - - if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") - # support polymorphic belongs_to as "label (Type)" - row[association.foreign_type] = $1 - end - - fk_type = reflection_class.type_for_attribute(fk_name).type - row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) - end - when :has_many - if association.options[:through] - add_join_records(row, HasManyThroughProxy.new(association)) - end - end - end + @model_metadata ||= ModelMetadata.new(model_class) end private - def build_table_rows_from(fixtures, config) + def build_table_rows_from(table_name, fixtures, config) now = config.default_timezone == :utc ? Time.now.utc : Time.now @tables[table_name] = fixtures.map do |label, fixture| @@ -99,31 +42,6 @@ module ActiveRecord ) end end - - def reflection_class_for(row) - if row.include?(model_metadata.inheritance_column_name) - row[model_metadata.inheritance_column_name].constantize rescue model_class - else - model_class - end - end - - def add_join_records(row, association) - # This is the case when the join table has no fixtures file - if (targets = row.delete(association.name.to_s)) - table_name = association.join_table - column_type = association.primary_key_type - lhs_key = association.lhs_key - rhs_key = association.rhs_key - - targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - joins = targets.map do |target| - { lhs_key => row[model_metadata.primary_key_name], - rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } - end - @tables[table_name].concat(joins) - end - end end end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b090c76a38..327121a2a2 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -519,11 +519,9 @@ module ActiveRecord def instantiate_fixtures(object, fixture_set, load_instances = true) return unless load_instances fixture_set.each do |fixture_name, fixture| - begin - object.instance_variable_set "@#{fixture_name}", fixture.find - rescue FixtureClassNotFound - nil - end + object.instance_variable_set "@#{fixture_name}", fixture.find + rescue FixtureClassNotFound + nil end end @@ -573,55 +571,54 @@ module ActiveRecord private - def read_and_insert(fixtures_directory, fixture_files, class_names, connection) # :nodoc: - fixtures_map = {} - fixture_sets = fixture_files.map do |fixture_set_name| - klass = class_names[fixture_set_name] - conn = klass&.connection || connection - fixtures_map[fixture_set_name] = new( # ActiveRecord::FixtureSet.new - conn, - fixture_set_name, - klass, - ::File.join(fixtures_directory, fixture_set_name) - ) - end - update_all_loaded_fixtures(fixtures_map) - - insert(fixture_sets, connection) + def read_and_insert(fixtures_directory, fixture_files, class_names, connection) # :nodoc: + fixtures_map = {} + fixture_sets = fixture_files.map do |fixture_set_name| + klass = class_names[fixture_set_name] + fixtures_map[fixture_set_name] = new( # ActiveRecord::FixtureSet.new + nil, + fixture_set_name, + klass, + ::File.join(fixtures_directory, fixture_set_name) + ) + end + update_all_loaded_fixtures(fixtures_map) - fixtures_map - end + insert(fixture_sets, connection) - def insert(fixture_sets, connection) # :nodoc: - fixture_sets_by_connection = fixture_sets.group_by do |fixture_set| - fixture_set.model_class&.connection || connection + fixtures_map end - fixture_sets_by_connection.each do |conn, set| - table_rows_for_connection = Hash.new { |h, k| h[k] = [] } + def insert(fixture_sets, connection) # :nodoc: + fixture_sets_by_connection = fixture_sets.group_by do |fixture_set| + fixture_set.model_class&.connection || connection + end + + fixture_sets_by_connection.each do |conn, set| + table_rows_for_connection = Hash.new { |h, k| h[k] = [] } - set.each do |fixture_set| - fixture_set.table_rows.each do |table, rows| - table_rows_for_connection[table].unshift(*rows) + set.each do |fixture_set| + fixture_set.table_rows.each do |table, rows| + table_rows_for_connection[table].unshift(*rows) + end end - end - conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys) + conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys) - # Cap primary key sequences to max(pk). - if conn.respond_to?(:reset_pk_sequence!) - set.each { |fs| conn.reset_pk_sequence!(fs.table_name) } + # 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 end - end - def update_all_loaded_fixtures(fixtures_map) # :nodoc: - all_loaded_fixtures.update(fixtures_map) - end + def update_all_loaded_fixtures(fixtures_map) # :nodoc: + all_loaded_fixtures.update(fixtures_map) + end end attr_reader :table_name, :name, :fixtures, :model_class, :config - def initialize(connection, name, class_name, path, config = ActiveRecord::Base) + def initialize(_, name, class_name, path, config = ActiveRecord::Base) @name = name @path = path @config = config @@ -630,11 +627,7 @@ module ActiveRecord @fixtures = read_fixture_files(path) - @connection = connection - - @table_name = (model_class.respond_to?(:table_name) ? - model_class.table_name : - self.class.default_fixture_table_name(name, config)) + @table_name = model_class&.table_name || self.class.default_fixture_table_name(name, config) end def [](x) diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 72035a986b..345441eea8 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -10,7 +10,7 @@ module ActiveRecord MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta2" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 138fd1cf53..9570bc6f86 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -249,7 +249,7 @@ module ActiveRecord sti_column = arel_attribute(inheritance_column, table) sti_names = ([self] + descendants).map(&:sti_name) - sti_column.in(sti_names) + predicate_builder.build(sti_column, sti_names) end # Detect the subclass from the inheritance column of attrs. If the inheritance column value diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 456689ec6d..fa6f0d36ec 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -20,7 +20,7 @@ module ActiveRecord # Indicates whether to use a stable #cache_key method that is accompanied # by a changing version in the #cache_version method. # - # This is +false+, by default until Rails 6.0. + # This is +true+, by default on Rails 5.2 and above. class_attribute :cache_versioning, instance_writer: false, default: false end @@ -61,23 +61,14 @@ module ActiveRecord # # Product.cache_versioning = false # Product.find(5).cache_key # => "products/5-20071224150000" (updated_at available) - def cache_key(*timestamp_names) + def cache_key if new_record? "#{model_name.cache_key}/new" else - if cache_version && timestamp_names.none? + if cache_version "#{model_name.cache_key}/#{id}" else - timestamp = if timestamp_names.any? - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Specifying a timestamp name for #cache_key has been deprecated in favor of - the explicit #cache_version method that can be overwritten. - MSG - - max_updated_column_timestamp(timestamp_names) - else - max_updated_column_timestamp - end + timestamp = max_updated_column_timestamp if timestamp timestamp = timestamp.utc.to_s(cache_timestamp_format) @@ -96,8 +87,19 @@ module ActiveRecord # Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to # +false+ (which it is by default until Rails 6.0). def cache_version - if cache_versioning && timestamp = try(:updated_at) - timestamp.utc.to_s(:usec) + return unless cache_versioning + + if has_attribute?("updated_at") + timestamp = updated_at_before_type_cast + if can_use_fast_cache_version?(timestamp) + raw_timestamp_to_cache_version(timestamp) + elsif timestamp = updated_at + timestamp.utc.to_s(cache_timestamp_format) + end + else + if self.class.has_attribute?("updated_at") + raise ActiveModel::MissingAttributeError, "missing attribute: updated_at" + end end end @@ -151,5 +153,43 @@ module ActiveRecord end end end + + private + # Detects if the value before type cast + # can be used to generate a cache_version. + # + # The fast cache version only works with a + # string value directly from the database. + # + # We also must check if the timestamp format has been changed + # or if the timezone is not set to UTC then + # we cannot apply our transformations correctly. + def can_use_fast_cache_version?(timestamp) + timestamp.is_a?(String) && + cache_timestamp_format == :usec && + default_timezone == :utc && + !updated_at_came_from_user? + end + + # Converts a raw database string to `:usec` + # format. + # + # Example: + # + # timestamp = "2018-10-15 20:02:15.266505" + # raw_timestamp_to_cache_version(timestamp) + # # => "20181015200215266505" + # + # Postgres truncates trailing zeros, + # https://github.com/postgres/postgres/commit/3e1beda2cde3495f41290e1ece5d544525810214 + # to account for this we pad the output with zeros + def raw_timestamp_to_cache_version(timestamp) + key = timestamp.delete("- :.") + if key.length < 20 + key.ljust(20, "0") + else + key + end + end end end diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 3626a13d7c..88b0c828ae 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -8,6 +8,10 @@ module ActiveRecord # as which environment migrations were run in. class InternalMetadata < ActiveRecord::Base # :nodoc: class << self + def _internal? + true + end + def primary_key "key" end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 5d1d15c94d..130ef8a330 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -14,9 +14,9 @@ module ActiveRecord # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example: # # Account.transaction do - # # select * from accounts where name = 'shugo' limit 1 for update - # shugo = Account.where("name = 'shugo'").lock(true).first - # yuko = Account.where("name = 'yuko'").lock(true).first + # # select * from accounts where name = 'shugo' limit 1 for update nowait + # shugo = Account.lock("FOR UPDATE NOWAIT").find_by(name: "shugo") + # yuko = Account.lock("FOR UPDATE NOWAIT").find_by(name: "yuko") # shugo.balance -= 100 # shugo.save! # yuko.balance += 100 diff --git a/activerecord/lib/active_record/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb new file mode 100644 index 0000000000..b5b5df074c --- /dev/null +++ b/activerecord/lib/active_record/middleware/database_selector.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "active_record/middleware/database_selector/resolver" + +module ActiveRecord + module Middleware + # The DatabaseSelector Middleware provides a framework for automatically + # swapping from the primary to the replica database connection. Rails + # provides a basic framework to determine when to swap and allows for + # applications to write custom strategy classes to override the default + # behavior. + # + # The resolver class defines when the application should switch (i.e. read + # from the primary if a write occurred less than 2 seconds ago) and a + # resolver context class that sets a value that helps the resolver class + # decide when to switch. + # + # Rails default middleware uses the request's session to set a timestamp + # that informs the application when to read from a primary or read from a + # replica. + # + # To use the DatabaseSelector in your application with default settings add + # the following options to your environment config: + # + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + # + # New applications will include these lines commented out in the production.rb. + # + # The default behavior can be changed by setting the config options to a + # custom class: + # + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = MyResolver + # config.active_record.database_resolver_context = MyResolver::MySession + class DatabaseSelector + def initialize(app, resolver_klass = Resolver, context_klass = Resolver::Session, options = {}) + @app = app + @resolver_klass = resolver_klass + @context_klass = context_klass + @options = options + end + + attr_reader :resolver_klass, :context_klass, :options + + # Middleware that determines which database connection to use in a multiple + # database application. + def call(env) + request = ActionDispatch::Request.new(env) + + select_database(request) do + @app.call(env) + end + end + + private + + def select_database(request, &blk) + context = context_klass.call(request) + resolver = resolver_klass.call(context, options) + + if reading_request?(request) + resolver.read(&blk) + else + resolver.write(&blk) + end + end + + def reading_request?(request) + request.get? || request.head? + end + end + end +end diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver.rb b/activerecord/lib/active_record/middleware/database_selector/resolver.rb new file mode 100644 index 0000000000..80b8cd7cae --- /dev/null +++ b/activerecord/lib/active_record/middleware/database_selector/resolver.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require "active_record/middleware/database_selector/resolver/session" + +module ActiveRecord + module Middleware + class DatabaseSelector + # The Resolver class is used by the DatabaseSelector middleware to + # determine which database the request should use. + # + # To change the behavior of the Resolver class in your application, + # create a custom resolver class that inherits from + # DatabaseSelector::Resolver and implements the methods that need to + # be changed. + # + # By default the Resolver class will send read traffic to the replica + # if it's been 2 seconds since the last write. + class Resolver # :nodoc: + SEND_TO_REPLICA_DELAY = 2.seconds + + def self.call(context, options = {}) + new(context, options) + end + + def initialize(context, options = {}) + @context = context + @options = options + @delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY + @instrumenter = ActiveSupport::Notifications.instrumenter + end + + attr_reader :context, :delay, :instrumenter + + def read(&blk) + if read_from_primary? + read_from_primary(&blk) + else + read_from_replica(&blk) + end + end + + def write(&blk) + write_to_primary(&blk) + end + + private + + def read_from_primary(&blk) + ActiveRecord::Base.connection.while_preventing_writes do + ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do + instrumenter.instrument("database_selector.active_record.read_from_primary") do + yield + end + end + end + end + + def read_from_replica(&blk) + ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role) do + instrumenter.instrument("database_selector.active_record.read_from_replica") do + yield + end + end + end + + def write_to_primary(&blk) + ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do + instrumenter.instrument("database_selector.active_record.wrote_to_primary") do + yield + ensure + context.update_last_write_timestamp + end + end + end + + def read_from_primary? + !time_since_last_write_ok? + end + + def send_to_replica_delay + delay + end + + def time_since_last_write_ok? + Time.now - context.last_write_timestamp >= send_to_replica_delay + end + end + end + end +end diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb new file mode 100644 index 0000000000..df7af054b7 --- /dev/null +++ b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module ActiveRecord + module Middleware + class DatabaseSelector + class Resolver + # The session class is used by the DatabaseSelector::Resolver to save + # timestamps of the last write in the session. + # + # The last_write is used to determine whether it's safe to read + # from the replica or the request needs to be sent to the primary. + class Session # :nodoc: + def self.call(request) + new(request.session) + end + + # Converts time to a timestamp that represents milliseconds since + # epoch. + def self.convert_time_to_timestamp(time) + time.to_i * 1000 + time.usec / 1000 + end + + # Converts milliseconds since epoch timestamp into a time object. + def self.convert_timestamp_to_time(timestamp) + timestamp ? Time.at(timestamp / 1000, (timestamp % 1000) * 1000) : Time.at(0) + end + + def initialize(session) + @session = session + end + + attr_reader :session + + def last_write_timestamp + self.class.convert_timestamp_to_time(session[:last_write]) + end + + def update_last_write_timestamp + session[:last_write] = self.class.convert_time_to_timestamp(Time.now) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 6e5a610642..c20274420f 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -23,7 +23,7 @@ module ActiveRecord # t.string :zipcode # end # - # execute <<-SQL + # execute <<~SQL # ALTER TABLE distributors # ADD CONSTRAINT zipchk # CHECK (char_length(zipcode) = 5) NO INHERIT; @@ -41,7 +41,7 @@ module ActiveRecord # t.string :zipcode # end # - # execute <<-SQL + # execute <<~SQL # ALTER TABLE distributors # ADD CONSTRAINT zipchk # CHECK (char_length(zipcode) = 5) NO INHERIT; @@ -49,7 +49,7 @@ module ActiveRecord # end # # def down - # execute <<-SQL + # execute <<~SQL # ALTER TABLE distributors # DROP CONSTRAINT zipchk # SQL @@ -68,7 +68,7 @@ module ActiveRecord # # reversible do |dir| # dir.up do - # execute <<-SQL + # execute <<~SQL # ALTER TABLE distributors # ADD CONSTRAINT zipchk # CHECK (char_length(zipcode) = 5) NO INHERIT; @@ -76,7 +76,7 @@ module ActiveRecord # end # # dir.down do - # execute <<-SQL + # execute <<~SQL # ALTER TABLE distributors # DROP CONSTRAINT zipchk # SQL @@ -1087,10 +1087,6 @@ module ActiveRecord 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) @@ -1122,11 +1118,6 @@ module ActiveRecord (db_list + file_list).sort_by { |_, version, _| version } end - def migration_files - paths = Array(migrations_paths) - Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] - end - def current_environment ActiveRecord::ConnectionHandling::DEFAULT_ENV.call end @@ -1145,6 +1136,15 @@ module ActiveRecord end private + def migration_files + paths = Array(migrations_paths) + Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] + end + + def parse_migration_filename(filename) + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + end + def move(direction, steps) migrator = Migrator.new(direction, migrations) @@ -1169,13 +1169,6 @@ module ActiveRecord class << self attr_accessor :migrations_paths - def migrations_path=(path) - ActiveSupport::Deprecation.warn \ - "`ActiveRecord::Migrator.migrations_path=` 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 @@ -1330,7 +1323,7 @@ module ActiveRecord def record_version_state_after_migrating(version) if down? migrated.delete(version) - ActiveRecord::SchemaMigration.where(version: version.to_s).delete_all + ActiveRecord::SchemaMigration.delete_by(version: version.to_s) else migrated << version ActiveRecord::SchemaMigration.create!(version: version.to_s) diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 8f6fcfcaea..abc939826b 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -16,13 +16,55 @@ module ActiveRecord V6_0 = Current class V5_2 < V6_0 + module TableDefinition + def timestamps(**options) + options[:precision] ||= nil + super + end + end + module CommandRecorder def invert_transaction(args, &block) [:transaction, args, block] end end + def create_table(table_name, **options) + if block_given? + super { |t| yield compatible_table_definition(t) } + else + super + end + end + + def change_table(table_name, **options) + if block_given? + super { |t| yield compatible_table_definition(t) } + else + super + end + end + + def create_join_table(table_1, table_2, **options) + if block_given? + super { |t| yield compatible_table_definition(t) } + else + super + end + end + + def add_timestamps(table_name, **options) + options[:precision] ||= nil + super + end + private + def compatible_table_definition(t) + class << t + prepend TableDefinition + end + t + end def command_recorder recorder = super @@ -35,20 +77,18 @@ module ActiveRecord 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) + if connection.adapter_name == "PostgreSQL" + super(table_name, column_name, type, options.except(:default, :null, :comment)) + connection.change_column_default(table_name, column_name, options[:default]) if options.key?(:default) + connection.change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + connection.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" + if connection.adapter_name == "Mysql2" super(table_name, options: "ENGINE=InnoDB", **options) else super @@ -70,13 +110,13 @@ module ActiveRecord end def create_table(table_name, options = {}) - if adapter_name == "PostgreSQL" + if connection.adapter_name == "PostgreSQL" if options[:id] == :uuid && !options.key?(:default) options[:default] = "uuid_generate_v4()" end end - unless adapter_name == "Mysql2" && options[:id] == :bigint + unless connection.adapter_name == "Mysql2" && options[:id] == :bigint if [:integer, :bigint].include?(options[:id]) && !options.key?(:default) options[:default] = nil end @@ -89,35 +129,12 @@ module ActiveRecord options[:id] = :integer end - if block_given? - super do |t| - yield compatible_table_definition(t) - end - else - super - end - end - - def change_table(table_name, options = {}) - if block_given? - super do |t| - yield compatible_table_definition(t) - end - else - super - end + super end def create_join_table(table_1, table_2, column_options: {}, **options) column_options.reverse_merge!(type: :integer) - - if block_given? - super do |t| - yield compatible_table_definition(t) - end - else - super - end + super end def add_column(table_name, column_name, type, options = {}) @@ -138,7 +155,7 @@ module ActiveRecord class << t prepend TableDefinition end - t + super end end @@ -156,33 +173,13 @@ module ActiveRecord end end - def create_table(table_name, options = {}) - if block_given? - super do |t| - yield compatible_table_definition(t) - end - else - super - end - end - - def change_table(table_name, options = {}) - if block_given? - super do |t| - yield compatible_table_definition(t) - end - else - super - end - end - - def add_reference(*, **options) + def add_reference(table_name, ref_name, **options) options[:index] ||= false super end alias :add_belongs_to :add_reference - def add_timestamps(_, **options) + def add_timestamps(table_name, **options) options[:null] = true if options[:null].nil? super end @@ -193,7 +190,7 @@ module ActiveRecord if options[:name].present? options[:name].to_s else - index_name(table_name, column: column_names) + connection.index_name(table_name, column: column_names) end super end @@ -213,15 +210,17 @@ module ActiveRecord end def index_name_for_remove(table_name, options = {}) - index_name = index_name(table_name, options) + index_name = connection.index_name(table_name, options) - unless index_name_exists?(table_name, index_name) + unless connection.index_name_exists?(table_name, index_name) if options.is_a?(Hash) && options.has_key?(:name) options_without_column = options.dup options_without_column.delete :column - index_name_without_column = index_name(table_name, options_without_column) + index_name_without_column = connection.index_name(table_name, options_without_column) - return index_name_without_column if index_name_exists?(table_name, index_name_without_column) + if connection.index_name_exists?(table_name, index_name_without_column) + return index_name_without_column + end end raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index c50a420432..55fc58e339 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -102,6 +102,21 @@ module ActiveRecord # If true, the default table name for a Product class will be "products". If false, it would just be "product". # See table_name for the full rules on table/class naming. This is true, by default. + ## + # :singleton-method: implicit_order_column + # :call-seq: implicit_order_column + # + # The name of the column records are ordered by if no explicit order clause + # is used during an ordered finder call. If not set the primary key is used. + + ## + # :singleton-method: implicit_order_column= + # :call-seq: implicit_order_column=(column_name) + # + # Sets the column to sort records by when no explicit order clause is used + # during an ordered finder call. Useful when the primary key is not an + # auto-incrementing integer, for example when it's a UUID. Note that using + # a non-unique column can result in non-deterministic results. included do mattr_accessor :primary_key_prefix_type, instance_writer: false @@ -110,6 +125,7 @@ module ActiveRecord 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 :pluralize_table_names, instance_writer: false, default: true + class_attribute :implicit_order_column, instance_accessor: false self.protected_environments = ["production"] self.inheritance_column = "type" diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 45ceb1e3ad..10148d0dca 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -96,11 +96,13 @@ 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, attributes) + def update(id = :all, attributes) if id.is_a?(Array) id.map { |one_id| find(one_id) }.each_with_index { |object, idx| object.update(attributes[idx]) } + elsif id == :all + all.each { |record| record.update(attributes) } else if ActiveRecord::Base === id raise ArgumentError, @@ -140,7 +142,7 @@ module ActiveRecord end end - # Deletes the row with a primary key matching the +id+ argument, using a + # Deletes the row with a primary key matching the +id+ argument, using an # SQL +DELETE+ statement, and returns the number of rows deleted. Active # Record objects are not instantiated, so the object's callbacks are not # executed, including any <tt>:dependent</tt> association options. @@ -159,7 +161,7 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) - where(primary_key => id_or_array).delete_all + delete_by(primary_key => id_or_array) end def _insert_record(values) # :nodoc: @@ -434,7 +436,7 @@ module ActiveRecord end alias update_attributes update - deprecate :update_attributes + deprecate update_attributes: "please, use update instead" # 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. @@ -448,7 +450,7 @@ module ActiveRecord end alias update_attributes! update! - deprecate :update_attributes! + deprecate update_attributes!: "please, use update! instead" # Equivalent to <code>update_columns(name => value)</code>. def update_column(name, value) @@ -479,15 +481,16 @@ module ActiveRecord verify_readonly_attribute(key.to_s) end + id_in_database = self.id_in_database + attributes.each do |k, v| + write_attribute_without_type_cast(k, v) + end + 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 - affected_rows == 1 end @@ -704,10 +707,10 @@ module ActiveRecord ) end - def create_or_update(*args, &block) + def create_or_update(**, &block) _raise_readonly_record_error if readonly? return false if destroyed? - result = new_record? ? _create_record(&block) : _update_record(*args, &block) + result = new_record? ? _create_record(&block) : _update_record(&block) result != false end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 28194c7c46..43a21e629e 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -26,15 +26,22 @@ module ActiveRecord end def self.run - ActiveRecord::Base.connection_handler.connection_pool_list. - reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! } + pools = [] + + ActiveRecord::Base.connection_handlers.each do |key, handler| + pools << handler.connection_pool_list.reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! } + end + + pools.flatten end 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? + ActiveRecord::Base.connection_handlers.each do |_, handler| + handler.connection_pool_list.each do |pool| + pool.release_connection if pool.active_connection? && !pool.connection.transaction_open? + end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index c84f3d0fbb..b3eeb60571 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -7,7 +7,7 @@ module ActiveRecord delegate :first_or_create, :first_or_create!, :first_or_initialize, 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 :destroy_all, :delete_all, :update_all, :destroy_by, :delete_by, to: :all delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, @@ -16,17 +16,17 @@ module ActiveRecord 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 - # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in + # be returned as an array, with the requested columns encapsulated as attributes of the model you call + # this method from. For example, if you call <tt>Product.find_by_sql</tt>, then the results will be returned in # a +Product+ object with the attributes you specified in the SQL query. # - # If you call a complicated SQL query which spans multiple tables the columns specified by the + # If you call a complicated SQL query which spans multiple tables, the columns specified by the # SELECT will be attributes of the model, whether or not they are columns of the corresponding # table. # - # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be - # no database agnostic conversions performed. This should be a last resort because using, for example, - # MySQL specific terms will lock you to using that particular database engine or require you to + # The +sql+ parameter is a full SQL query as a string. It will be called as is; there will be + # no database agnostic conversions performed. This should be a last resort because using + # database-specific terms will lock you into using that particular database engine, or require you to # change your call if you switch engines. # # # A simple SQL query spanning multiple tables @@ -40,7 +40,7 @@ module ActiveRecord def find_by_sql(sql, binds = [], preparable: nil, &block) result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable) column_types = result_set.column_types.dup - columns_hash.each_key { |k| column_types.delete k } + attribute_types.each_key { |k| column_types.delete k } message_bus = ActiveSupport::Notifications.instrumenter payload = { @@ -60,7 +60,9 @@ module ActiveRecord # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. # The use of this method should be restricted to complicated SQL queries that can't be executed - # using the ActiveRecord::Calculations class methods. Look into those before using this. + # using the ActiveRecord::Calculations class methods. Look into those before using this method, + # as it could lock you into a specific database engine or require a code change to switch + # database engines. # # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" # # => 12 diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 538659d6bd..a1d7c893bf 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -88,6 +88,14 @@ module ActiveRecord end end + initializer "active_record.database_selector" do + if options = config.active_record.delete(:database_selector) + resolver = config.active_record.delete(:database_resolver) + operations = config.active_record.delete(:database_resolver_context) + config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations, options + end + end + initializer "Check for cache versioning support" do config.after_initialize do |app| ActiveSupport.on_load(:active_record) do @@ -140,7 +148,19 @@ end_error initializer "active_record.define_attribute_methods" do |app| config.after_initialize do ActiveSupport.on_load(:active_record) do - descendants.each(&:define_attribute_methods) if app.config.eager_load + if app.config.eager_load + descendants.each do |model| + # SchemaMigration and InternalMetadata both override `table_exists?` + # to bypass the schema cache, so skip them to avoid the extra queries. + next if model._internal? + + # If there's no connection yet, or the schema cache doesn't have the columns + # hash for the model cached, `define_attribute_methods` would trigger a query. + next unless model.connected? && model.connection.schema_cache.columns_hash?(model.table_name) + + model.define_attribute_methods + end + end end end end @@ -155,8 +175,18 @@ end_error initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - configs = app.config.active_record.dup + configs = app.config.active_record + + represent_boolean_as_integer = configs.sqlite3.delete(:represent_boolean_as_integer) + + unless represent_boolean_as_integer.nil? + ActiveSupport.on_load(:active_record_sqlite3adapter) do + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = represent_boolean_as_integer + end + end + configs.delete(:sqlite3) + configs.each do |k, v| send "#{k}=", v end @@ -167,6 +197,7 @@ end_error # and then establishes the connection. initializer "active_record.initialize_database" do ActiveSupport.on_load(:active_record) do + self.connection_handlers = { writing_role => ActiveRecord::Base.default_connection_handler } self.configurations = Rails.application.config.database_configuration establish_connection end @@ -224,35 +255,6 @@ end_error end end - initializer "active_record.check_represent_sqlite3_boolean_as_integer" do - config.after_initialize do - ActiveSupport.on_load(:active_record_sqlite3adapter) do - represent_boolean_as_integer = Rails.application.config.active_record.sqlite3.delete(:represent_boolean_as_integer) - unless represent_boolean_as_integer.nil? - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = represent_boolean_as_integer - end - - unless ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer - ActiveSupport::Deprecation.warn <<-MSG -Leaving `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer` -set to false is deprecated. SQLite databases have used 't' and 'f' to serialize -boolean values and must have old data converted to 1 and 0 (its native boolean -serialization) before setting this flag to true. Conversion can be accomplished -by setting up a rake task which runs - - ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1) - ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0) - -for all models and all boolean columns, after which the flag must be set to -true by adding the following to your application.rb file: - - Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true -MSG - end - end - end - end - initializer "active_record.set_filter_attributes" do ActiveSupport.on_load(:active_record) do self.filter_attributes += Rails.application.config.filter_parameters diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb index b5129e4239..d57680aaaa 100644 --- a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb +++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb @@ -3,7 +3,7 @@ module ActiveRecord module Railties # :nodoc: module CollectionCacheAssociationLoading #:nodoc: - def setup(context, options, block) + def setup(context, options, as, block) @relation = relation_from_options(options) super @@ -20,12 +20,12 @@ module ActiveRecord end end - def collection_without_template + def collection_without_template(*) @relation.preload_associations(@collection) if @relation super end - def collection_with_template + def collection_with_template(*) @relation.preload_associations(@collection) if @relation super end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 1c7ceb4981..8de06e8466 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -191,11 +191,9 @@ db_namespace = namespace :db do # desc "Retrieves the collation for the current environment's database" task collation: :load_config do - begin - puts ActiveRecord::Tasks::DatabaseTasks.collation_current - rescue NoMethodError - $stderr.puts "Sorry, your database adapter is not supported yet. Feel free to submit a patch." - end + puts ActiveRecord::Tasks::DatabaseTasks.collation_current + rescue NoMethodError + $stderr.puts "Sorry, your database adapter is not supported yet. Feel free to submit a patch." end desc "Retrieves the current schema version number" @@ -300,15 +298,22 @@ db_namespace = namespace :db do namespace :cache do desc "Creates a db/schema_cache.yml file." 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) + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name) + ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache( + ActiveRecord::Base.connection, + filename, + ) + end end desc "Clears a db/schema_cache.yml file." task clear: :load_config do - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") - rm_f filename, verbose: false + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name) + rm_f filename, verbose: false + end end end end @@ -354,17 +359,15 @@ db_namespace = namespace :db do # desc "Recreate the test database from an existent schema.rb file" task load_schema: %w(db:test:purge) do - begin - should_reconnect = ActiveRecord::Base.connection_pool.active_connection? - ActiveRecord::Schema.verbose = false - ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| - filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby) - ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :ruby, filename, "test") - end - ensure - if should_reconnect - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations.default_hash(ActiveRecord::Tasks::DatabaseTasks.env)) - end + should_reconnect = ActiveRecord::Base.connection_pool.active_connection? + ActiveRecord::Schema.verbose = false + ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :ruby, filename, "test") + end + ensure + if should_reconnect + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations.default_hash(ActiveRecord::Tasks::DatabaseTasks.env)) end end @@ -404,6 +407,10 @@ namespace :railties do if railtie.respond_to?(:paths) && (path = railtie.paths["db/migrate"].first) railties[railtie.railtie_name] = path end + + unless ENV["MIGRATIONS_PATH"].blank? + railties[railtie.railtie_name] = railtie.root + ENV["MIGRATIONS_PATH"] + end end on_skip = Proc.new do |name, migration| diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index b2110f727c..3452cf971b 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -178,28 +178,24 @@ module ActiveRecord scope ? [scope] : [] end - def build_join_constraint(table, foreign_table) - key = join_keys.key - foreign_key = join_keys.foreign_key - - constraint = table[key].eq(foreign_table[foreign_key]) - - if klass.finder_needs_type_condition? - table.create_and([constraint, klass.send(:type_condition, table)]) - else - constraint - end - end - - def join_scope(table, foreign_klass) + def join_scope(table, foreign_table, foreign_klass) predicate_builder = predicate_builder(table) scope_chain_items = join_scopes(table, predicate_builder) klass_scope = klass_join_scope(table, predicate_builder) + key = join_keys.key + foreign_key = join_keys.foreign_key + + klass_scope.where!(table[key].eq(foreign_table[foreign_key])) + if type klass_scope.where!(type => foreign_klass.polymorphic_name) end + if klass.finder_needs_type_condition? + klass_scope.where!(klass.send(:type_condition, table)) + end + scope_chain_items.inject(klass_scope, &:merge!) end @@ -612,21 +608,9 @@ module ActiveRecord # returns either +nil+ or the inverse association name that it finds. def automatic_inverse_of - return unless can_find_inverse_of_automatically?(self) + if can_find_inverse_of_automatically?(self) + inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym - inverse_name_candidates = - if options[:as] - [options[:as]] - else - active_record_name = active_record.name.demodulize - [active_record_name, ActiveSupport::Inflector.pluralize(active_record_name)] - end - - inverse_name_candidates.map! do |candidate| - ActiveSupport::Inflector.underscore(candidate).to_sym - end - - inverse_name_candidates.detect do |inverse_name| begin reflection = klass._reflect_on_association(inverse_name) rescue NameError @@ -635,7 +619,9 @@ module ActiveRecord reflection = false end - valid_inverse_reflection?(reflection) + if valid_inverse_reflection?(reflection) + return inverse_name + end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index d5b6082d13..d612ff53c1 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -44,6 +44,11 @@ module ActiveRecord end def bind_attribute(name, value) # :nodoc: + if reflection = klass._reflect_on_association(name) + name = reflection.foreign_key + value = value.read_attribute(reflection.klass.primary_key) unless value.nil? + end + attr = arel_attribute(name) bind = predicate_builder.build_bind_attribute(attr.name, value) yield attr, bind @@ -62,6 +67,7 @@ module ActiveRecord # user = users.new { |user| user.name = 'Oscar' } # user.name # => Oscar def new(attributes = nil, &block) + block = _deprecated_scope_block("new", &block) scoping { klass.new(attributes, &block) } end @@ -87,7 +93,12 @@ module ActiveRecord # users.create(name: nil) # validation on name # # => #<User id: nil, name: nil, ...> def create(attributes = nil, &block) - scoping { klass.create(attributes, &block) } + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr, &block) } + else + block = _deprecated_scope_block("create", &block) + scoping { klass.create(attributes, &block) } + end end # Similar to #create, but calls @@ -97,7 +108,12 @@ module ActiveRecord # Expects arguments in the same format as # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]. def create!(attributes = nil, &block) - scoping { klass.create!(attributes, &block) } + if attributes.is_a?(Array) + attributes.collect { |attr| create!(attr, &block) } + else + block = _deprecated_scope_block("create!", &block) + scoping { klass.create!(attributes, &block) } + end end def first_or_create(attributes = nil, &block) # :nodoc: @@ -163,7 +179,7 @@ module ActiveRecord # 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. + # 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 @@ -173,7 +189,7 @@ module ActiveRecord # # * 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 + # 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, @@ -307,12 +323,12 @@ 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 - @delegate_to_klass ? yield : klass._scoping(self) { yield } + already_in_scope? ? yield : _scoping(self) { yield } end - def _exec_scope(*args, &block) # :nodoc: + def _exec_scope(name, *args, &block) # :nodoc: @delegate_to_klass = true - instance_exec(*args, &block) || self + _scoping(_deprecated_spawn(name)) { instance_exec(*args, &block) || self } ensure @delegate_to_klass = false end @@ -356,6 +372,12 @@ module ActiveRecord stmt.wheres = arel.constraints if updates.is_a?(Hash) + if klass.locking_enabled? && + !updates.key?(klass.locking_column) && + !updates.key?(klass.locking_column.to_sym) + attr = arel_attribute(klass.locking_column) + updates[attr.name] = _increment_attribute(attr) + end stmt.set _substitute_values(updates) else stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name)) @@ -378,10 +400,7 @@ module ActiveRecord updates = {} counters.each do |counter_name, value| attr = arel_attribute(counter_name) - bind = predicate_builder.build_bind_attribute(attr.name, value.abs) - expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attr), 0) - expr = value < 0 ? expr - bind : expr + bind - updates[counter_name] = expr.expr + updates[attr.name] = _increment_attribute(attr, value) end if touch @@ -417,12 +436,7 @@ module ActiveRecord # 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) - if klass.locking_enabled? - names << { time: time } - update_counters(klass.locking_column => 1, touch: names) - else - update_all klass.touch_attributes_with_time(*names, time: time) - end + update_all klass.touch_attributes_with_time(*names, time: time) end # Destroys the records by instantiating each @@ -491,6 +505,32 @@ module ActiveRecord affected end + # Finds and destroys all records matching the specified conditions. + # This is short-hand for <tt>relation.where(condition).destroy_all</tt>. + # Returns the collection of objects that were destroyed. + # + # If no record is found, returns empty array. + # + # Person.destroy_by(id: 13) + # Person.destroy_by(name: 'Spartacus', rating: 4) + # Person.destroy_by("published_at < ?", 2.weeks.ago) + def destroy_by(*args) + where(*args).destroy_all + end + + # Finds and deletes all records matching the specified conditions. + # This is short-hand for <tt>relation.where(condition).delete_all</tt>. + # Returns the number of rows affected. + # + # If no record is found, returns <tt>0</tt> as zero rows were affected. + # + # Person.delete_by(id: 13) + # Person.delete_by(name: 'Spartacus', rating: 4) + # Person.delete_by("published_at < ?", 2.weeks.ago) + def delete_by(*args) + where(*args).delete_all + end + # Causes the records to be loaded from the database if they have not # been loaded already. You can use this if for some reason you need # to explicitly load some records before actually using them. The @@ -511,6 +551,7 @@ module ActiveRecord def reset @delegate_to_klass = false + @_deprecated_scope_source = nil @to_sql = @arel = @loaded = @should_eager_load = nil @records = [].freeze @offsets = {} @@ -619,7 +660,10 @@ module ActiveRecord end end + attr_reader :_deprecated_scope_source # :nodoc: + protected + attr_writer :_deprecated_scope_source # :nodoc: def load_records(records) @records = records.freeze @@ -627,6 +671,31 @@ module ActiveRecord end private + def already_in_scope? + @delegate_to_klass && begin + scope = klass.current_scope(true) + scope && !scope._deprecated_scope_source + end + end + + def _deprecated_spawn(name) + spawn.tap { |scope| scope._deprecated_scope_source = name } + end + + def _deprecated_scope_block(name, &block) + -> record do + klass.current_scope = _deprecated_spawn(name) + yield record if block_given? + end + end + + def _scoping(scope) + previous, klass.current_scope = klass.current_scope(true), scope + yield + ensure + klass.current_scope = previous + end + def _substitute_values(values) values.map do |name, value| attr = arel_attribute(name) @@ -638,6 +707,13 @@ module ActiveRecord end end + def _increment_attribute(attribute, value = 1) + bind = predicate_builder.build_bind_attribute(attribute.name, value.abs) + expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attribute), 0) + expr = value < 0 ? expr - bind : expr + bind + expr.expr + end + def exec_queries(&block) skip_query_cache_if_necessary do @records = diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 0fa5ba2e50..4f9ddf302e 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -41,15 +41,13 @@ module ActiveRecord def count(column_name = nil) if block_given? unless column_name.nil? - ActiveSupport::Deprecation.warn \ - "When `count' is called with a block, it ignores other arguments. " \ - "This behavior is now deprecated and will result in an ArgumentError in Rails 6.0." + raise ArgumentError, "Column name argument is not supported when a block is passed." end - return super() + super() + else + calculate(:count, column_name) end - - calculate(:count, column_name) end # Calculates the average value on a given column. Returns +nil+ if there's @@ -86,15 +84,13 @@ module ActiveRecord def sum(column_name = nil) if block_given? unless column_name.nil? - ActiveSupport::Deprecation.warn \ - "When `sum' is called with a block, it ignores other arguments. " \ - "This behavior is now deprecated and will result in an ArgumentError in Rails 6.0." + raise ArgumentError, "Column name argument is not supported when a block is passed." end - return super() + super() + else + calculate(:sum, column_name) end - - calculate(:sum, column_name) end # This calculates aggregate values in the given column. Methods for #count, #sum, #average, @@ -190,11 +186,9 @@ module ActiveRecord relation = apply_join_dependency relation.pluck(*column_names) else - disallow_raw_sql!(column_names) + klass.disallow_raw_sql!(column_names) relation = spawn - relation.select_values = column_names.map { |cn| - @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn - } + relation.select_values = column_names result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil) } result.cast_values(klass.attribute_types) end @@ -227,7 +221,6 @@ module ActiveRecord end private - def has_include?(column_name) eager_loading? || (includes_values.present? && column_name && column_name != :all) end @@ -242,10 +235,12 @@ module ActiveRecord if operation == "count" column_name ||= select_for_count if column_name == :all - if distinct && (group_values.any? || select_values.empty? && order_values.empty?) + if !distinct + distinct = distinct_select?(select_for_count) if group_values.empty? + elsif group_values.any? || select_values.empty? && order_values.empty? column_name = primary_key end - elsif /\s*DISTINCT[\s(]+/i.match?(column_name.to_s) + elsif distinct_select?(column_name) distinct = nil end end @@ -257,6 +252,10 @@ module ActiveRecord end end + def distinct_select?(column_name) + column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name) + end + def aggregate_column(column_name) return column_name if Arel::Expressions === column_name @@ -401,7 +400,7 @@ module ActiveRecord case operation when "count" then value.to_i when "sum" then type.deserialize(value || 0) - when "average" then value.respond_to?(:to_d) ? value.to_d : value + when "average" then value&.respond_to?(:to_d) ? value.to_d : value else type.deserialize(value) end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 383dc1bf4b..7a53a9d1c7 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "mutex_m" + module ActiveRecord module Delegation # :nodoc: module DelegateCache # :nodoc: @@ -31,6 +33,10 @@ module ActiveRecord super end + def generate_relation_method(method) + generated_relation_methods.generate_method(method) + end + protected def include_relation_methods(delegate) superclass.include_relation_methods(delegate) unless base_class? @@ -39,27 +45,32 @@ module ActiveRecord private def generated_relation_methods - @generated_relation_methods ||= Module.new.tap do |mod| - mod_name = "GeneratedRelationMethods" - const_set mod_name, mod - private_constant mod_name - end + @generated_relation_methods ||= GeneratedRelationMethods.new end + end + + class GeneratedRelationMethods < Module # :nodoc: + include Mutex_m + + def generate_method(method) + synchronize do + return if method_defined?(method) - def generate_relation_method(method) if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) - generated_relation_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(*args, &block) scoping { klass.#{method}(*args, &block) } end RUBY else - generated_relation_methods.send(:define_method, method) do |*args, &block| + define_method(method) do |*args, &block| scoping { klass.public_send(method, *args, &block) } end end end + end end + private_constant :GeneratedRelationMethods extend ActiveSupport::Concern @@ -78,49 +89,18 @@ module ActiveRecord module ClassSpecificRelation # :nodoc: extend ActiveSupport::Concern - included do - @delegation_mutex = Mutex.new - end - module ClassMethods # :nodoc: def name superclass.name end - - def delegate_to_scoped_klass(method) - @delegation_mutex.synchronize do - return if method_defined?(method) - - if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) - module_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{method}(*args, &block) - scoping { @klass.#{method}(*args, &block) } - end - RUBY - else - define_method method do |*args, &block| - scoping { @klass.public_send(method, *args, &block) } - end - end - end - end end private def method_missing(method, *args, &block) if @klass.respond_to?(method) - self.class.delegate_to_scoped_klass(method) + @klass.generate_relation_method(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." - arel.public_send(method, *args, &block) else super end @@ -141,7 +121,7 @@ module ActiveRecord private def respond_to_missing?(method, _) - super || @klass.respond_to?(method) || arel.respond_to?(method) + super || @klass.respond_to?(method) end end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 6f420fe6bb..74f323a278 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -7,8 +7,8 @@ module ActiveRecord ONE_AS_ONE = "1 AS one" # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). - # If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key - # is an integer, find by id coerces its arguments using +to_i+. + # If one or more records can not be found for the requested ids, then ActiveRecord::RecordNotFound will be raised. + # If the primary key is an integer, find by id coerces its arguments by using +to_i+. # # Person.find(1) # returns the object for ID = 1 # Person.find("1") # returns the object for ID = 1 @@ -79,17 +79,12 @@ module ActiveRecord # Post.find_by "published_at < ?", 2.weeks.ago def find_by(arg, *args) where(arg, *args).take - rescue ::RangeError - nil end # Like #find_by, except that if no record is found, raises # an ActiveRecord::RecordNotFound error. def find_by!(arg, *args) where(arg, *args).take! - rescue ::RangeError - raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", - @klass.name, @klass.primary_key) end # Gives a record (or N records if a parameter is supplied) without any implied @@ -319,9 +314,7 @@ module ActiveRecord relation = construct_relation_for_exists(conditions) - skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists") } ? true : false - rescue ::RangeError - false + skip_query_cache_if_necessary { connection.select_one(relation.arel, "#{name} Exists") } ? true : false end # This method is called whenever no records are found with either a single @@ -359,11 +352,17 @@ module ActiveRecord end def construct_relation_for_exists(conditions) - relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) + conditions = sanitize_forbidden_attributes(conditions) + + if distinct_value && offset_value + relation = limit(1) + else + relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) + end case conditions when Array, Hash - relation.where!(conditions) + relation.where!(conditions) unless conditions.empty? else relation.where!(primary_key => conditions) unless conditions == :none end @@ -432,9 +431,6 @@ module ActiveRecord else find_some(ids) end - rescue ::RangeError - 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) @@ -550,8 +546,8 @@ module ActiveRecord end def ordered_relation - if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) + if order_values.empty? && (implicit_order_column || primary_key) + order(arel_attribute(implicit_order_column || primary_key).asc) else self end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index b59ff912fe..240de3bb69 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -90,16 +90,21 @@ module ActiveRecord queries.reduce(&:or) 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) + values = value.nil? ? [nil] : Array.wrap(value) + if mapping.length == 1 || values.empty? + column_name, aggr_attr = mapping.first + values = values.map do |object| + object.respond_to?(aggr_attr) ? object.public_send(aggr_attr) : object + end + build(table.arel_attribute(column_name), values) + else + queries = values.map do |object| + mapping.map do |field_attr, aggregate_attr| + build(table.arel_attribute(field_attr), object.try!(aggregate_attr)) + end.reduce(&:and) + end + queries.reduce(&:or) end - queries.reduce(&:or) else build(table.arel_attribute(key), value) end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index fadb3c420d..ee2ece1560 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -22,9 +22,8 @@ module ActiveRecord when 1 then predicate_builder.build(attribute, values.first) else values.map! do |v| - bind = predicate_builder.build_bind_attribute(attribute.name, v) - bind if bind.value.boundable? - end.compact! + predicate_builder.build_bind_attribute(attribute.name, v) + end values.empty? ? NullPredicate : attribute.in(values) 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 44bb2c7ab6..2ea27c8490 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -3,11 +3,7 @@ module ActiveRecord class PredicateBuilder class RangeHandler # :nodoc: - class RangeWithBinds < Struct.new(:begin, :end) - def exclude_end? - false - end - end + RangeWithBinds = Struct.new(:begin, :end, :exclude_end?) def initialize(predicate_builder) @predicate_builder = predicate_builder @@ -16,22 +12,7 @@ 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 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 end_bind.value.infinity? - attribute.gteq(begin_bind) - elsif value.exclude_end? - attribute.gteq(begin_bind).and(attribute.lt(end_bind)) - else - attribute.between(RangeWithBinds.new(begin_bind, end_bind)) - end + attribute.between(RangeWithBinds.new(begin_bind, end_bind, value.exclude_end?)) end private diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index f64bd30d38..cd18f27330 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -18,24 +18,31 @@ module ActiveRecord end def nil? - !value_before_type_cast.is_a?(StatementCache::Substitute) && - (value_before_type_cast.nil? || value_for_database.nil?) + unless value_before_type_cast.is_a?(StatementCache::Substitute) + value_before_type_cast.nil? || + type.respond_to?(:subtype, true) && value_for_database.nil? + end + rescue ::RangeError end - def boundable? - return @_boundable if defined?(@_boundable) - nil? - @_boundable = true + def infinite? + infinity?(value_before_type_cast) || infinity?(value_for_database) rescue ::RangeError - @_boundable = false end - def infinity? - _infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database) + def unboundable? + if defined?(@_unboundable) + @_unboundable + else + value_for_database unless value_before_type_cast.is_a?(StatementCache::Substitute) + @_unboundable = nil + end + rescue ::RangeError + @_unboundable = type.cast(value_before_type_cast) <=> 0 end private - def _infinity?(value) + def infinity?(value) value.respond_to?(:infinite?) && value.infinite? end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index b41cbf6f48..f88493df8a 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1073,11 +1073,13 @@ module ActiveRecord def arel_columns(columns) 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 + case field + when Symbol + field = field.to_s + arel_column(field) { connection.quote_table_name(field) } + when String + arel_column(field) { field } + when Proc field.call else field @@ -1085,6 +1087,18 @@ module ActiveRecord end end + def arel_column(field) + field = klass.attribute_alias(field) if klass.attribute_alias?(field) + from = from_clause.name || from_clause.value + + if klass.columns_hash.key?(field) && + (!from || from == table.name || from == connection.quote_table_name(table.name)) + arel_attribute(field) + else + yield + end + end + def reverse_sql_order(order_query) if order_query.empty? return [arel_attribute(primary_key).desc] if primary_key @@ -1120,7 +1134,7 @@ module ActiveRecord # Uses SQL function with multiple arguments. (order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) || # Uses "nulls first" like construction. - /nulls (first|last)\Z/i.match?(order) + /\bnulls\s+(?:first|last)\b/i.match?(order) end def build_order(arel) @@ -1166,14 +1180,20 @@ module ActiveRecord order_args.map! do |arg| case arg when Symbol - arel_attribute(arg).asc + arg = arg.to_s + arel_column(arg) { + Arel.sql(connection.quote_table_name(arg)) + }.asc when Hash arg.map { |field, dir| case field when Arel::Nodes::SqlLiteral field.send(dir.downcase) else - arel_attribute(field).send(dir.downcase) + field = field.to_s + arel_column(field) { + Arel.sql(connection.quote_table_name(field)) + }.send(dir.downcase) end } else diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 562e04194c..efc4b447aa 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -8,7 +8,7 @@ module ActiveRecord module SpawnMethods # This is overridden by Associations::CollectionProxy def spawn #:nodoc: - clone + already_in_scope? ? klass.all : clone end # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation. diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index a502713e56..47728aac30 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -125,6 +125,10 @@ module ActiveRecord raise ArgumentError, "Invalid argument for .where.not(), got nil." when Arel::Nodes::In Arel::Nodes::NotIn.new(node.left, node.right) + when Arel::Nodes::IsNotDistinctFrom + Arel::Nodes::IsDistinctFrom.new(node.left, node.right) + when Arel::Nodes::IsDistinctFrom + Arel::Nodes::IsNotDistinctFrom.new(node.left, node.right) when Arel::Nodes::Equality Arel::Nodes::NotEqual.new(node.left, node.right) when String @@ -136,11 +140,7 @@ module ActiveRecord def except_predicates(columns) predicates.reject do |node| - case node - when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual - subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) - columns.include?(subrelation.name.to_s) - end + Arel.fetch_attribute(node) { |attr| columns.include?(attr.name.to_s) } end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 3485d9e557..e6197752bc 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -134,43 +134,6 @@ module ActiveRecord 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. - # - # Given: - # - # class Person < ActiveRecord::Base - # composed_of :address, class_name: "Address", - # mapping: [%w(address_street street), %w(address_city city)] - # end - # - # Then: - # - # { address: Address.new("813 abc st.", "chicago") } - # # => { address_street: "813 abc st.", address_city: "chicago" } - def expand_hash_conditions_for_aggregates(attrs) # :doc: - expanded_attrs = {} - attrs.each do |attr, value| - if aggregation = reflect_on_aggregation(attr.to_sym) - mapping = aggregation.mapping - mapping.each do |field_attr, aggregate_attr| - 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 - value.send(aggregate_attr) - end - end - else - expanded_attrs[attr] = value - end - end - expanded_attrs - end - deprecate :expand_hash_conditions_for_aggregates - def replace_bind_variables(statement, values) raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size) bound = values.dup diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 216359867c..76bf53387d 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -51,20 +51,11 @@ module ActiveRecord if info[:version].present? ActiveRecord::SchemaMigration.create_table - connection.assume_migrated_upto_version(info[:version], migrations_paths) + connection.assume_migrated_upto_version(info[:version]) end ActiveRecord::InternalMetadata.create_table ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment end - - private - # Returns the migrations paths. - # - # ActiveRecord::Schema.new.migrations_paths - # # => ["db/migrate"] # Rails migration path by default. - def migrations_paths - ActiveRecord::Migrator.migrations_paths - end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index f2d8b038fa..1fca1a18f6 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -10,6 +10,10 @@ module ActiveRecord # to be executed the next time. class SchemaMigration < ActiveRecord::Base # :nodoc: class << self + def _internal? + true + end + def primary_key "version" end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 9eba1254a4..35e9dcbffc 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -23,14 +23,13 @@ module ActiveRecord current_scope end - private - def current_scope(skip_inherited_scope = false) - ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) - end + def current_scope(skip_inherited_scope = false) + ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) + end - def current_scope=(scope) - ScopeRegistry.set_value_for(:current_scope, self, scope) - end + def current_scope=(scope) + ScopeRegistry.set_value_for(:current_scope, self, scope) + end end def populate_with_current_scope_attributes # :nodoc: diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 6caf9b3251..de75fbe127 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -31,14 +31,7 @@ module ActiveRecord # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } def unscoped - block_given? ? _scoping(relation) { yield } : relation - end - - def _scoping(relation) # :nodoc: - previous, self.current_scope = current_scope(true), relation - yield - ensure - self.current_scope = previous + block_given? ? relation.scoping { yield } : relation end # Are there attributes associated with this scope? @@ -107,7 +100,7 @@ module ActiveRecord self.default_scopes += [scope] end - def build_default_scope(base_rel = nil) + def build_default_scope(relation = relation()) return if abstract_class? if default_scope_override.nil? @@ -118,15 +111,14 @@ 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) + relation.merge!(scope) end end elsif default_scopes.any? - base_rel ||= relation evaluate_default_scope do - default_scopes.inject(base_rel) do |default_scope, scope| + default_scopes.inject(relation) do |default_scope, scope| scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call) - default_scope.merge!(base_rel.instance_exec(&scope)) + default_scope.instance_exec(&scope) || default_scope end end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index d5cc5db97e..681a5c6250 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -27,6 +27,14 @@ module ActiveRecord scope = current_scope if scope + if scope._deprecated_scope_source + ActiveSupport::Deprecation.warn(<<~MSG.squish) + Class level methods will no longer inherit scoping from `#{scope._deprecated_scope_source}` + in Rails 6.1. To continue using the scoped relation, pass it into the block directly. + To instead access the full set of models, as Rails 6.1 will, use `#{name}.unscoped`. + MSG + end + if self == scope.klass scope.clone else @@ -179,13 +187,13 @@ module ActiveRecord extension = Module.new(&block) if block if body.respond_to?(:to_proc) - singleton_class.send(:define_method, name) do |*args| - scope = all._exec_scope(*args, &body) + singleton_class.define_method(name) do |*args| + scope = all._exec_scope(name, *args, &body) scope = scope.extending(extension) if extension scope end else - singleton_class.send(:define_method, name) do |*args| + singleton_class.define_method(name) do |*args| scope = body.call(*args) || all scope = scope.extending(extension) if extension scope diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 1b1736dcab..95984e7ada 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -132,6 +132,8 @@ module ActiveRecord sql = query_builder.sql_for bind_values, connection klass.find_by_sql(sql, bind_values, preparable: true, &block) + rescue ::RangeError + nil end def self.unsupported_value?(value) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 974d7a1c0a..27e401a756 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -313,6 +313,16 @@ module ActiveRecord ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) end + def cache_dump_filename(namespace) + filename = if namespace == "primary" + "schema_cache.yml" + else + "#{namespace}_schema_cache.yml" + end + + ENV["SCHEMA_CACHE"] || 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, spec_name, env| load_schema(configuration, format, file, env, spec_name) diff --git a/activerecord/lib/active_record/test_fixtures.rb b/activerecord/lib/active_record/test_fixtures.rb index 7b7b3f7112..8c60d71669 100644 --- a/activerecord/lib/active_record/test_fixtures.rb +++ b/activerecord/lib/active_record/test_fixtures.rb @@ -122,7 +122,7 @@ module ActiveRecord # Begin transactions for connections already established @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| - connection.begin_transaction joinable: false + connection.begin_transaction joinable: false, _lazy: false connection.pool.lock_thread = true if lock_threads end @@ -138,7 +138,7 @@ module ActiveRecord end if connection && !@fixture_connections.include?(connection) - connection.begin_transaction joinable: false + connection.begin_transaction joinable: false, _lazy: false connection.pool.lock_thread = true if lock_threads @fixture_connections << connection end @@ -173,10 +173,33 @@ module ActiveRecord end def enlist_fixture_connections + setup_shared_connection_pool + ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection) end private + + # Shares the writing connection pool with connections on + # other handlers. + # + # In an application with a primary and replica the test fixtures + # need to share a connection pool so that the reading connection + # can see data in the open transaction on the writing connection. + def setup_shared_connection_pool + writing_handler = ActiveRecord::Base.connection_handler + + ActiveRecord::Base.connection_handlers.values.each do |handler| + if handler != writing_handler + handler.connection_pool_list.each do |pool| + name = pool.spec.name + writing_connection = writing_handler.retrieve_connection_pool(name) + handler.send(:owner_to_pool)[name] = writing_connection + end + end + end + end + def load_fixtures(config) fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config) Hash[fixtures.map { |f| [f.name, f] }] diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index d32f971ad1..04a1c03474 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -56,7 +56,7 @@ module ActiveRecord 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) + attribute_names.index_with(time || current_time_from_proper_timezone) end private @@ -101,8 +101,8 @@ module ActiveRecord super end - def _update_record(*args, touch: true, **options) - if touch && should_record_timestamps? + def _update_record + if @_touch_record && should_record_timestamps? current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each do |column| @@ -110,7 +110,13 @@ module ActiveRecord _write_attribute(column, current_time) end end - super(*args) + + super + end + + def create_or_update(touch: true, **) + @_touch_record = touch + super end def should_record_timestamps? @@ -133,11 +139,10 @@ module ActiveRecord self.class.send(:current_time_from_proper_timezone) end - def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update_in_model) - timestamp_names - .map { |attr| self[attr] } + def max_updated_column_timestamp + timestamp_attributes_for_update_in_model + .map { |attr| self[attr]&.to_time } .compact - .map(&:to_time) .max end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index c5d5fca672..fe3842b905 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -375,10 +375,6 @@ module ActiveRecord raise ActiveRecord::Rollback unless status end status - ensure - if @transaction_state && @transaction_state.committed? - clear_transaction_record_state - end end private diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index c303186ef2..03d00006b7 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -48,12 +48,11 @@ module ActiveRecord private - def current_adapter_name - ActiveRecord::Base.connection.adapter_name.downcase.to_sym - end + def current_adapter_name + ActiveRecord::Base.connection.adapter_name.downcase.to_sym + end end - Helpers = ActiveModel::Type::Helpers BigInteger = ActiveModel::Type::BigInteger Binary = ActiveModel::Type::Binary Boolean = ActiveModel::Type::Boolean diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5a1dbc8e53..88ae62c681 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -12,7 +12,7 @@ module ActiveRecord raise ArgumentError, "#{options[:scope]} is not supported format for :scope option. " \ "Pass a symbol or an array of symbols instead: `scope: :user_id`" end - super({ case_sensitive: true }.merge!(options)) + super @klass = options[:class] end @@ -56,33 +56,23 @@ module ActiveRecord end def build_relation(klass, attribute, value) - if reflection = klass._reflect_on_association(attribute) - attribute = reflection.foreign_key - value = value.attributes[reflection.klass.primary_key] unless value.nil? - end - - if value.nil? - return klass.unscoped.where!(attribute => value) - end - - # the attribute may be an aliased attribute - if klass.attribute_alias?(attribute) - attribute = klass.attribute_alias(attribute) + relation = klass.unscoped + comparison = relation.bind_attribute(attribute, value) do |attr, bind| + return relation.none! if bind.unboundable? + + if bind.nil? + attr.eq(bind) + elsif !options.key?(:case_sensitive) + klass.connection.default_uniqueness_comparison(attr, bind) + elsif options[:case_sensitive] + klass.connection.case_sensitive_comparison(attr, bind) + else + # will use SQL LOWER function before comparison, unless it detects a case insensitive collation + klass.connection.case_insensitive_comparison(attr, bind) + end end - attribute_name = attribute.to_s - value = klass.predicate_builder.build_bind_attribute(attribute_name, value) - - table = klass.arel_table - column = klass.columns_hash[attribute_name] - - comparison = if !options[:case_sensitive] - # will use SQL LOWER function before comparison, unless it detects a case insensitive collation - klass.connection.case_insensitive_comparison(table, attribute, column, value) - else - klass.connection.case_sensitive_comparison(table, attribute, column, value) - end - klass.unscoped.where!(comparison) + relation.where!(comparison) end def scope_relation(record, relation) diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb index dab785738e..361cd915cc 100644 --- a/activerecord/lib/arel.rb +++ b/activerecord/lib/arel.rb @@ -13,7 +13,6 @@ 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" @@ -40,6 +39,13 @@ module Arel # :nodoc: all value.is_a?(Arel::Node) || value.is_a?(Arel::Attribute) || value.is_a?(Arel::Nodes::SqlLiteral) end + def self.fetch_attribute(value) + case value + 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 + yield value.left.is_a?(Arel::Attributes::Attribute) ? value.left : value.right + end + end + ## Convenience Alias Node = Arel::Nodes::Node end diff --git a/activerecord/lib/arel/compatibility/wheres.rb b/activerecord/lib/arel/compatibility/wheres.rb deleted file mode 100644 index c8a73f0dae..0000000000 --- a/activerecord/lib/arel/compatibility/wheres.rb +++ /dev/null @@ -1,35 +0,0 @@ -# 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/nodes/and.rb b/activerecord/lib/arel/nodes/and.rb index c530a77bfb..bf516db35f 100644 --- a/activerecord/lib/arel/nodes/and.rb +++ b/activerecord/lib/arel/nodes/and.rb @@ -2,7 +2,7 @@ module Arel # :nodoc: all module Nodes - class And < Arel::Nodes::Node + class And < Arel::Nodes::NodeExpression attr_reader :children def initialize(children) diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb index 91e9b2b70f..344e46479f 100644 --- a/activerecord/lib/arel/nodes/bind_param.rb +++ b/activerecord/lib/arel/nodes/bind_param.rb @@ -23,6 +23,14 @@ module Arel # :nodoc: all def nil? value.nil? end + + def infinite? + value.respond_to?(:infinite?) && value.infinite? + end + + def unboundable? + value.respond_to?(:unboundable?) && value.unboundable? + end end end end diff --git a/activerecord/lib/arel/nodes/case.rb b/activerecord/lib/arel/nodes/case.rb index 654a54825e..1c4b727bf6 100644 --- a/activerecord/lib/arel/nodes/case.rb +++ b/activerecord/lib/arel/nodes/case.rb @@ -2,7 +2,7 @@ module Arel # :nodoc: all module Nodes - class Case < Arel::Nodes::Node + class Case < Arel::Nodes::NodeExpression attr_accessor :case, :conditions, :default def initialize(expression = nil, default = nil) diff --git a/activerecord/lib/arel/nodes/casted.rb b/activerecord/lib/arel/nodes/casted.rb index c1e6e97d6d..6e911b717d 100644 --- a/activerecord/lib/arel/nodes/casted.rb +++ b/activerecord/lib/arel/nodes/casted.rb @@ -27,6 +27,10 @@ module Arel # :nodoc: all class Quoted < Arel::Nodes::Unary # :nodoc: alias :val :value def nil?; val.nil?; end + + def infinite? + value.respond_to?(:infinite?) && value.infinite? + end end def self.build_quoted(other, attribute = nil) diff --git a/activerecord/lib/arel/nodes/equality.rb b/activerecord/lib/arel/nodes/equality.rb index 2aa85a977e..551d56c2ff 100644 --- a/activerecord/lib/arel/nodes/equality.rb +++ b/activerecord/lib/arel/nodes/equality.rb @@ -7,5 +7,12 @@ module Arel # :nodoc: all alias :operand1 :left alias :operand2 :right end + + %w{ + IsDistinctFrom + IsNotDistinctFrom + }.each do |name| + const_set name, Class.new(Equality) + end end end diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb index e83a6f162f..7dafde4952 100644 --- a/activerecord/lib/arel/predications.rb +++ b/activerecord/lib/arel/predications.rb @@ -18,6 +18,14 @@ module Arel # :nodoc: all Nodes::Equality.new self, quoted_node(other) end + def is_not_distinct_from(other) + Nodes::IsNotDistinctFrom.new self, quoted_node(other) + end + + def is_distinct_from(other) + Nodes::IsDistinctFrom.new self, quoted_node(other) + end + def eq_any(others) grouping_any :eq, others end @@ -27,15 +35,17 @@ module Arel # :nodoc: all end def between(other) - if equals_quoted?(other.begin, -Float::INFINITY) - if equals_quoted?(other.end, Float::INFINITY) + if unboundable?(other.begin) == 1 || unboundable?(other.end) == -1 + self.in([]) + elsif open_ended?(other.begin) + if other.end.nil? || open_ended?(other.end) not_in([]) elsif other.exclude_end? lt(other.end) else lteq(other.end) end - elsif equals_quoted?(other.end, Float::INFINITY) + elsif other.end.nil? || open_ended?(other.end) gteq(other.begin) elsif other.exclude_end? gteq(other.begin).and(lt(other.end)) @@ -73,15 +83,17 @@ Passing a range to `#in` is deprecated. Call `#between`, instead. end def not_between(other) - if equals_quoted?(other.begin, -Float::INFINITY) - if equals_quoted?(other.end, Float::INFINITY) + if unboundable?(other.begin) == 1 || unboundable?(other.end) == -1 + not_in([]) + elsif open_ended?(other.begin) + if other.end.nil? || open_ended?(other.end) self.in([]) elsif other.exclude_end? gteq(other.end) else gt(other.end) end - elsif equals_quoted?(other.end, Float::INFINITY) + elsif other.end.nil? || open_ended?(other.end) lt(other.begin) else left = lt(other.begin) @@ -230,12 +242,16 @@ Passing a range to `#not_in` is deprecated. Call `#not_between`, instead. 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 + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end + + def unboundable?(value) + value.respond_to?(:unboundable?) && value.unboundable? + end + + def open_ended?(value) + infinity?(value) || unboundable?(value) end end end diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb index 8f65d303ac..92d309453c 100644 --- a/activerecord/lib/arel/visitors/depth_first.rb +++ b/activerecord/lib/arel/visitors/depth_first.rb @@ -95,6 +95,8 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_NotEqual :binary alias :visit_Arel_Nodes_NotIn :binary alias :visit_Arel_Nodes_NotRegexp :binary + alias :visit_Arel_Nodes_IsNotDistinctFrom :binary + alias :visit_Arel_Nodes_IsDistinctFrom :binary alias :visit_Arel_Nodes_Or :binary alias :visit_Arel_Nodes_OuterJoin :binary alias :visit_Arel_Nodes_Regexp :binary diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb index 9054f0159b..6389c875cb 100644 --- a/activerecord/lib/arel/visitors/dot.rb +++ b/activerecord/lib/arel/visitors/dot.rb @@ -195,6 +195,8 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_JoinSource :binary alias :visit_Arel_Nodes_LessThan :binary alias :visit_Arel_Nodes_LessThanOrEqual :binary + alias :visit_Arel_Nodes_IsNotDistinctFrom :binary + alias :visit_Arel_Nodes_IsDistinctFrom :binary alias :visit_Arel_Nodes_Matches :binary alias :visit_Arel_Nodes_NotEqual :binary alias :visit_Arel_Nodes_NotIn :binary diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb index 0a06aef60b..73166054da 100644 --- a/activerecord/lib/arel/visitors/ibm_db.rb +++ b/activerecord/lib/arel/visitors/ibm_db.rb @@ -10,6 +10,12 @@ module Arel # :nodoc: all collector = visit o.expr, collector collector << " ROWS ONLY" end + + def is_distinct_from(o, collector) + collector << "DECODE(" + collector = visit [o.left, o.right, 0, 1], collector + collector << ")" + end end end end diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb index 0a9713794e..208fa15aef 100644 --- a/activerecord/lib/arel/visitors/informix.rb +++ b/activerecord/lib/arel/visitors/informix.rb @@ -15,8 +15,9 @@ module Arel # :nodoc: all collector << "ORDER BY " collector = inject_join o.orders, collector, ", " end - collector = maybe_visit o.lock, 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? diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb index d564e19089..fdd864b40d 100644 --- a/activerecord/lib/arel/visitors/mssql.rb +++ b/activerecord/lib/arel/visitors/mssql.rb @@ -12,6 +12,31 @@ module Arel # :nodoc: all private + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + right = o.right + + if right.nil? + collector = visit o.left, collector + collector << " IS NULL" + else + collector << "EXISTS (VALUES (" + collector = visit o.left, collector + collector << ") INTERSECT VALUES (" + collector = visit right, collector + collector << "))" + end + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + if o.right.nil? + collector = visit o.left, collector + collector << " IS NOT NULL" + else + collector << "NOT " + visit_Arel_Nodes_IsNotDistinctFrom o, collector + end + end + def visit_Arel_Visitors_MSSQL_RowNumber(o, collector) collector << "ROW_NUMBER() OVER (ORDER BY " inject_join(o.children, collector, ", ") << ") as _row_num" diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb index 32f6705d04..dd77cfdf66 100644 --- a/activerecord/lib/arel/visitors/mysql.rb +++ b/activerecord/lib/arel/visitors/mysql.rb @@ -4,34 +4,6 @@ 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 - - case o.left - when Arel::Nodes::Union - visit_Arel_Nodes_Union o.left, collector, true - else - visit o.left, collector - end - - collector << " UNION " - - 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 @@ -65,6 +37,17 @@ module Arel # :nodoc: all collector end + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " <=> " + visit o.right, collector + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + collector << "NOT " + visit_Arel_Nodes_IsNotDistinctFrom o, collector + end + # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support # these, we must use a subquery. diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb index 30a1529d46..f96bf65ee5 100644 --- a/activerecord/lib/arel/visitors/oracle.rb +++ b/activerecord/lib/arel/visitors/oracle.rb @@ -148,6 +148,12 @@ module Arel # :nodoc: all def visit_Arel_Nodes_BindParam(o, collector) collector.add_bind(o.value) { |i| ":a#{i}" } end + + def is_distinct_from(o, collector) + collector << "DECODE(" + collector = visit [o.left, o.right, 0, 1], collector + collector << ")" + end end end end diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb index 7061f06087..9a7fe4d626 100644 --- a/activerecord/lib/arel/visitors/oracle12.rb +++ b/activerecord/lib/arel/visitors/oracle12.rb @@ -20,7 +20,7 @@ module Arel # :nodoc: all 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 + maybe_visit o.lock, collector end def visit_Arel_Nodes_Limit(o, collector) @@ -56,6 +56,12 @@ module Arel # :nodoc: all def visit_Arel_Nodes_BindParam(o, collector) collector.add_bind(o.value) { |i| ":a#{i}" } end + + def is_distinct_from(o, collector) + collector << "DECODE(" + collector = visit [o.left, o.right, 0, 1], collector + collector << ")" + end end end end diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb index c5110fa89c..920776b4dc 100644 --- a/activerecord/lib/arel/visitors/postgresql.rb +++ b/activerecord/lib/arel/visitors/postgresql.rb @@ -77,6 +77,18 @@ module Arel # :nodoc: all grouping_parentheses o, collector end + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS NOT DISTINCT FROM " + visit o.right, collector + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS DISTINCT FROM " + visit o.right, collector + end + # Used by Lateral visitor to enclose select queries in parentheses def grouping_parentheses(o, collector) if o.expr.is_a? Nodes::SelectStatement diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb index cb1d2424ad..af6f7e856a 100644 --- a/activerecord/lib/arel/visitors/sqlite.rb +++ b/activerecord/lib/arel/visitors/sqlite.rb @@ -22,6 +22,18 @@ module Arel # :nodoc: all def visit_Arel_Nodes_False(o, collector) collector << "0" end + + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS " + visit o.right, collector + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS NOT " + visit o.right, collector + end end end end diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 7ce26884a5..d0dec63860 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -208,14 +208,12 @@ module Arel # :nodoc: all 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 + maybe_visit o.lock, collector end def visit_Arel_Nodes_SelectCore(o, collector) @@ -268,13 +266,11 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_Union(o, collector) - collector << "( " - infix_value(o, collector, " UNION ") << " )" + infix_value_with_paren(o, collector, " UNION ") end def visit_Arel_Nodes_UnionAll(o, collector) - collector << "( " - infix_value(o, collector, " UNION ALL ") << " )" + infix_value_with_paren(o, collector, " UNION ALL ") end def visit_Arel_Nodes_Intersect(o, collector) @@ -579,6 +575,10 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_In(o, collector) + if Array === o.right && !o.right.empty? + o.right.delete_if { |value| unboundable?(value) } + end + if Array === o.right && o.right.empty? collector << "1=0" else @@ -589,6 +589,10 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_NotIn(o, collector) + if Array === o.right && !o.right.empty? + o.right.delete_if { |value| unboundable?(value) } + end + if Array === o.right && o.right.empty? collector << "1=1" else @@ -625,6 +629,8 @@ module Arel # :nodoc: all def visit_Arel_Nodes_Equality(o, collector) right = o.right + return collector << "1=0" if unboundable?(right) + collector = visit o.left, collector if right.nil? @@ -635,9 +641,31 @@ module Arel # :nodoc: all end end + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + if o.right.nil? + collector = visit o.left, collector + collector << " IS NULL" + else + collector = is_distinct_from(o, collector) + collector << " = 0" + end + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + if o.right.nil? + collector = visit o.left, collector + collector << " IS NOT NULL" + else + collector = is_distinct_from(o, collector) + collector << " = 1" + end + end + def visit_Arel_Nodes_NotEqual(o, collector) right = o.right + return collector << "1=1" if unboundable?(right) + collector = visit o.left, collector if right.nil? @@ -788,6 +816,10 @@ module Arel # :nodoc: all } end + def unboundable?(value) + value.respond_to?(:unboundable?) && value.unboundable? + end + def has_join_sources?(o) o.relation.is_a?(Nodes::JoinSource) && !o.relation.right.empty? end @@ -833,6 +865,23 @@ module Arel # :nodoc: all visit o.right, collector end + def infix_value_with_paren(o, collector, value, suppress_parens = false) + collector << "( " unless suppress_parens + collector = if o.left.class == o.class + infix_value_with_paren(o.left, collector, value, true) + else + visit o.left, collector + end + collector << value + collector = if o.right.class == o.class + infix_value_with_paren(o.right, collector, value, true) + else + visit o.right, collector + end + collector << " )" unless suppress_parens + collector + end + def aggregate(name, o, collector) collector << "#{name}(" if o.distinct @@ -846,6 +895,19 @@ module Arel # :nodoc: all collector end end + + def is_distinct_from(o, collector) + collector << "CASE WHEN " + collector = visit o.left, collector + collector << " = " + collector = visit o.right, collector + collector << " OR (" + collector = visit o.left, collector + collector << " IS NULL AND " + collector = visit o.right, collector + collector << " IS NULL)" + collector << " THEN 0 ELSE 1 END" + end end end end |