diff options
Diffstat (limited to 'activerecord/lib')
164 files changed, 1469 insertions, 1155 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 3250e29b82..aa08124158 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -14,7 +14,6 @@ module ActiveRecord end private - def clear_aggregation_cache @aggregation_cache.clear if persisted? end diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 4c538ef2bd..de9892e48d 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -29,7 +29,6 @@ module ActiveRecord end private - def exec_queries super do |record| @association.set_inverse_instance_from_queries(record) diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 272eede824..ac90ba0137 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -72,7 +72,6 @@ module ActiveRecord attr_reader :aliases private - def truncate(name) name.slice(0, @connection.table_alias_length - 2) end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 3b4b243148..0c61094d6c 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -128,5 +128,9 @@ module ActiveRecord::Associations::Builder # :nodoc: name = reflection.name model.before_destroy lambda { |o| o.association(name).handle_dependency } end + + private_class_method :build_scope, :macro, :valid_options, :validate_options, :define_extensions, + :define_callbacks, :define_accessors, :define_readers, :define_writers, :define_validations, + :valid_dependent_options, :check_dependent_options, :add_destroy_callbacks end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index fc00f1e900..321ccba918 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -74,11 +74,11 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.add_touch_callbacks(model, reflection) foreign_key = reflection.foreign_key - n = reflection.name + name = reflection.name touch = reflection.options[:touch] callback = lambda { |changes_method| lambda { |record| - BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method) + BelongsTo.touch_record(record, record.send(changes_method), foreign_key, name, touch, belongs_to_touch_method) }} if reflection.counter_cache_column @@ -123,5 +123,8 @@ module ActiveRecord::Associations::Builder # :nodoc: model.validates_presence_of reflection.name, message: :required end end + + private_class_method :macro, :valid_options, :valid_dependent_options, :define_callbacks, :define_validations, + :add_counter_cache_callbacks, :add_touch_callbacks, :add_default_callbacks, :add_destroy_callbacks end end diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 9fccfcce0c..e78d25441b 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -22,9 +22,9 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.define_extensions(model, name, &block) if block_given? - extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" + extension_module_name = "#{name.to_s.camelize}AssociationExtension" extension = Module.new(&block) - model.module_parent.const_set(extension_module_name, extension) + model.const_set(extension_module_name, extension) end end @@ -66,5 +66,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end CODE end + + private_class_method :valid_options, :define_callback, :define_extensions, :define_readers, :define_writers end end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 0140aa15c8..6ad4c75fb5 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -46,7 +46,6 @@ module ActiveRecord::Associations::Builder # :nodoc: end private - def self.suppress_composite_primary_key(pk) pk unless pk.is_a?(Array) end @@ -73,7 +72,6 @@ module ActiveRecord::Associations::Builder # :nodoc: end private - def middle_options(join_model) middle_options = {} middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}" diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 5b9617bc6d..556e2988f5 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -13,5 +13,7 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.valid_dependent_options [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception] end + + private_class_method :macro, :valid_options, :valid_dependent_options end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index bfb37d6eee..27ebe8cb71 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -7,7 +7,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.valid_options(options) - valid = super + [:as] + valid = super + [:as, :touch] valid += [:through, :source, :source_type] if options[:through] valid end @@ -16,6 +16,11 @@ module ActiveRecord::Associations::Builder # :nodoc: [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception] end + def self.define_callbacks(model, reflection) + super + add_touch_callbacks(model, reflection) if reflection.options[:touch] + end + def self.add_destroy_callbacks(model, reflection) super unless reflection.options[:through] end @@ -26,5 +31,34 @@ module ActiveRecord::Associations::Builder # :nodoc: model.validates_presence_of reflection.name, message: :required end end + + def self.touch_record(o, name, touch) + record = o.send name + + return unless record && record.persisted? + + if touch != true + record.touch(touch) + else + record.touch + end + end + + def self.add_touch_callbacks(model, reflection) + name = reflection.name + touch = reflection.options[:touch] + + callback = lambda { |record| + HasOne.touch_record(record, name, touch) + } + + model.after_create callback, if: :saved_changes? + model.after_update callback, if: :saved_changes? + model.after_destroy callback + model.after_touch callback + end + + private_class_method :macro, :valid_options, :valid_dependent_options, :add_destroy_callbacks, + :define_callbacks, :define_validations, :add_touch_callbacks end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 0a02ef4cc1..0e22563b41 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -38,5 +38,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end CODE end + + private_class_method :valid_options, :define_accessors, :define_constructors end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index c3d4eab562..891b50d160 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -56,7 +56,7 @@ module ActiveRecord def ids_writer(ids) primary_key = reflection.association_primary_key pk_type = klass.type_for_attribute(primary_key) - ids = Array(ids).reject(&:blank?) + ids = Array(ids).compact_blank ids.map! { |i| pk_type.cast(i) } records = klass.where(primary_key => ids).index_by do |r| diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index edcb44f0fc..0db0ad8595 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -1002,7 +1002,7 @@ module ActiveRecord end # Adds one or more +records+ to the collection by setting their foreign keys - # to the association's primary key. Since +<<+ flattens its argument list and + # to the association's primary key. Since <tt><<</tt> flattens its argument list and # inserts each record, +push+ and +concat+ behave identically. Returns +self+ # so several appends may be chained together. # @@ -1029,7 +1029,7 @@ module ActiveRecord alias_method :append, :<< alias_method :concat, :<< - def prepend(*args) + def prepend(*args) # :nodoc: raise NoMethodError, "prepend on association is not defined. Please use <<, push or append" end @@ -1101,7 +1101,6 @@ module ActiveRecord delegate(*delegate_methods, to: :scope) private - def find_nth_with_limit(index, limit) load_target if find_from_target? super diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 5972846940..dd2ed55279 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -37,7 +37,6 @@ module ActiveRecord end private - # Returns the number of records in this collection. # # If the association has a counter cache it gets that value. Otherwise diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index b76005b587..f35a40fb2f 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -64,16 +64,17 @@ module ActiveRecord end end - def initialize(base, table, associations) + def initialize(base, table, associations, join_type) tree = self.class.make_tree associations @join_root = JoinBase.new(base, table, build(tree, base)) + @join_type = join_type end def reflections join_root.drop(1).map!(&:reflection) end - def join_constraints(joins_to_add, join_type, alias_tracker) + def join_constraints(joins_to_add, alias_tracker) @alias_tracker = alias_tracker construct_tables!(join_root) @@ -82,9 +83,9 @@ module ActiveRecord joins.concat joins_to_add.flat_map { |oj| construct_tables!(oj.join_root) if join_root.match? oj.join_root - walk join_root, oj.join_root + walk(join_root, oj.join_root, oj.join_type) else - make_join_constraints(oj.join_root, join_type) + make_join_constraints(oj.join_root, oj.join_type) end } end @@ -125,7 +126,7 @@ module ActiveRecord end protected - attr_reader :join_root + attr_reader :join_root, :join_type private attr_reader :alias_tracker @@ -151,7 +152,7 @@ module ActiveRecord end end - def make_constraints(parent, child, join_type = Arel::Nodes::OuterJoin) + def make_constraints(parent, child, join_type) foreign_table = parent.table foreign_klass = parent.base_klass joins = child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) @@ -173,13 +174,13 @@ module ActiveRecord join ? "#{name}_join" : name end - def walk(left, right) + def walk(left, right, join_type) intersection, missing = right.children.map { |node1| [left.children.find { |node2| node1.match? node2 }, node1] }.partition(&:first) - joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r) } - joins.concat missing.flat_map { |_, n| make_constraints(left, n) } + joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r, join_type) } + joins.concat missing.flat_map { |_, n| make_constraints(left, n, join_type) } end def find_reflection(klass, name) 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 ca0305abbb..6a7e92dc28 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -44,8 +44,7 @@ module ActiveRecord unless others.empty? joins.concat arel.join_sources - right = joins.last.right - right.expr.children.concat(others) + append_constraints(joins.last, others) end # The current table in this iteration becomes the foreign table in the next @@ -65,6 +64,16 @@ module ActiveRecord @readonly = reflection.scope && reflection.scope_for(base_klass.unscoped).readonly_value end + + private + def append_constraints(join, constraints) + if join.is_a?(Arel::Nodes::StringJoin) + join_string = table.create_and(constraints.unshift(join.left)) + join.left = Arel.sql(base_klass.connection.visitor.compile(join_string)) + else + join.right.expr.children.concat(constraints) + end + end end end end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 6b57e5093a..d4e8b364e1 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -95,7 +95,6 @@ module ActiveRecord end private - # Loads all the given data into +records+ for the +association+. def preloaders_on(association, records, scope, polymorphic_parent = false) case association diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 46532f651e..4c7b0e6f07 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -27,7 +27,9 @@ module ActiveRecord end def records_by_owner - @records_by_owner ||= preloaded_records.each_with_object({}) do |record, result| + # owners can be duplicated when a relation has a collection association join + # #compare_by_identity makes such owners different hash keys + @records_by_owner ||= preloaded_records.each_with_object({}.compare_by_identity) do |record, result| owners_by_key[convert_key(record[association_key_name])].each do |owner| (result[owner] ||= []) << record end @@ -36,13 +38,7 @@ module ActiveRecord def preloaded_records return @preloaded_records if defined?(@preloaded_records) - return [] if owner_keys.empty? - # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) - # Make several smaller queries if necessary or make one query if the adapter supports it - slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) - @preloaded_records = slices.flat_map do |slice| - records_for(slice) - end + @preloaded_records = owner_keys.empty? ? [] : records_for(owner_keys) end private diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 929045f29b..acb8ba7e5a 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -7,7 +7,6 @@ module ActiveRecord include ActiveModel::AttributeAssignment private - def _assign_attributes(attributes) multi_parameter_attributes = {} nested_parameter_attributes = {} diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 98b7805c0a..0b66043d2a 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -46,7 +46,6 @@ module ActiveRecord end private - def load_schema! super attribute_types.each do |name, type| @@ -75,7 +74,6 @@ module ActiveRecord end private - def decorators_for(name, type) matching(name, type).map(&:last) end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index af7e46e649..21f72bb6c7 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -24,7 +24,7 @@ module ActiveRecord RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) - class GeneratedAttributeMethodsBuilder < Module #:nodoc: + class GeneratedAttributeMethods < Module #:nodoc: include Mutex_m end @@ -35,7 +35,7 @@ module ActiveRecord end def initialize_generated_modules # :nodoc: - @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethodsBuilder.new) + @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethods.new) private_constant :GeneratedAttributeMethods @attribute_methods_generated = false include @generated_attribute_methods @@ -89,7 +89,7 @@ module ActiveRecord # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass # defines its own attribute method, then we don't want to overwrite that. defined = method_defined_within?(method_name, superclass, Base) && - ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethodsBuilder) + ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods) defined || super end end @@ -159,57 +159,6 @@ module ActiveRecord end end - # Regexp for column names (with or without a table name prefix). Matches - # the following: - # "#{table_name}.#{column_name}" - # "#{column_name}" - COLUMN_NAME = /\A(?:\w+\.)?\w+\z/i - - # Regexp for column names with order (with or without a table name - # prefix, with or without various order modifiers). Matches the following: - # "#{table_name}.#{column_name}" - # "#{table_name}.#{column_name} #{direction}" - # "#{table_name}.#{column_name} #{direction} NULLS FIRST" - # "#{table_name}.#{column_name} NULLS LAST" - # "#{column_name}" - # "#{column_name} #{direction}" - # "#{column_name} #{direction} NULLS FIRST" - # "#{column_name} NULLS LAST" - COLUMN_NAME_WITH_ORDER = / - \A - (?:\w+\.)? - \w+ - (?:\s+asc|\s+desc)? - (?:\s+nulls\s+(?:first|last))? - \z - /ix - - def disallow_raw_sql!(args, permit: COLUMN_NAME) # :nodoc: - unexpected = args.reject do |arg| - Arel.arel_node?(arg) || - arg.to_s.split(/\s*,\s*/).all? { |part| permit.match?(part) } - end - - return if unexpected.none? - - if allow_unsafe_raw_sql == :deprecated - ActiveSupport::Deprecation.warn( - "Dangerous query method (method whose arguments are used as raw " \ - "SQL) called with non-attribute argument(s): " \ - "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \ - "arguments will be disallowed in Rails 6.0. This method should " \ - "not be called with user-provided values, such as request " \ - "parameters or model attributes. Known-safe values can be passed " \ - "by wrapping them in Arel.sql()." - ) - else - raise(ActiveRecord::UnknownAttributeReference, - "Query method called with non-attribute argument(s): " + - unexpected.map(&:inspect).join(", ") - ) - end - end - # Returns true if the given attribute exists, otherwise false. # # class Person < ActiveRecord::Base @@ -437,7 +386,7 @@ module ActiveRecord def attributes_for_update(attribute_names) attribute_names &= self.class.column_names attribute_names.delete_if do |name| - readonly_attribute?(name) + self.class.readonly_attribute?(name) end end @@ -460,12 +409,8 @@ module ActiveRecord end end - def readonly_attribute?(name) - self.class.readonly_attributes.include?(name) - end - def pk_attribute?(name) - name == self.class.primary_key + name == @primary_key end end end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index dc239ff9ea..4a7b6c60e5 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -46,6 +46,7 @@ module ActiveRecord # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) + sync_with_transaction_state if @transaction_state&.finalized? @attributes[attr_name.to_s].value_before_type_cast end @@ -60,17 +61,18 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast + sync_with_transaction_state if @transaction_state&.finalized? @attributes.values_before_type_cast end private - # Dispatch target for <tt>*_before_type_cast</tt> attribute methods. def attribute_before_type_cast(attribute_name) read_attribute_before_type_cast(attribute_name) end def attribute_came_from_user?(attribute_name) + sync_with_transaction_state if @transaction_state&.finalized? @attributes[attribute_name].came_from_user? end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 68ac8475b0..45341765c1 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -156,13 +156,19 @@ module ActiveRecord end private + def mutations_from_database + sync_with_transaction_state if @transaction_state&.finalized? + super + end + + def mutations_before_last_save + sync_with_transaction_state if @transaction_state&.finalized? + super + end + 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 = super + clear_attribute_change(attr_name) result end @@ -171,6 +177,11 @@ module ActiveRecord affected_rows = super + if @_skip_dirty_tracking ||= false + clear_attribute_changes(@_touch_attr_names) + return affected_rows + end + changes = {} @attributes.keys.each do |attr_name| next if @_touch_attr_names.include?(attr_name) @@ -187,7 +198,7 @@ module ActiveRecord affected_rows ensure - @_touch_attr_names = nil + @_touch_attr_names, @_skip_dirty_tracking = nil, nil end def _update_record(attribute_names = attribute_names_for_partial_writes) diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 6af5346fa7..768c5f8c05 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -16,44 +16,35 @@ module ActiveRecord # 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 + _read_attribute(@primary_key) end # 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 + _write_attribute(@primary_key, value) end # Queries the primary key column's value. def id? - sync_with_transaction_state - query_attribute(self.class.primary_key) + query_attribute(@primary_key) end # 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) + read_attribute_before_type_cast(@primary_key) end # Returns the primary key column's previous value. def id_was - sync_with_transaction_state - attribute_was(self.class.primary_key) + attribute_was(@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) + attribute_in_database(@primary_key) end private - def attribute_method?(attr_name) attr_name == "id" || super end @@ -122,13 +113,12 @@ module ActiveRecord # # Project.primary_key # => "foo_id" def primary_key=(value) - @primary_key = value && value.to_s + @primary_key = value && -value.to_s @quoted_primary_key = nil @attributes_builder = nil end private - def suppress_composite_primary_key(pk) return pk unless pk.is_a?(Array) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index ffac5313ad..0f0e721b24 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -7,16 +7,12 @@ module ActiveRecord module ClassMethods # :nodoc: private - def define_method_attribute(name) - sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key - ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( generated_attribute_methods, name ) do |temp_method_name, attr_name_expr| generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{temp_method_name} - #{sync_with_transaction_state} name = #{attr_name_expr} _read_attribute(name) { |n| missing_attribute(n, caller) } end @@ -30,19 +26,16 @@ module ActiveRecord # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name, &block) name = attr_name.to_s - if self.class.attribute_alias?(name) - name = self.class.attribute_alias(name) - end + name = self.class.attribute_aliases[name] || name - primary_key = self.class.primary_key - name = primary_key if name == "id" && primary_key - sync_with_transaction_state if name == primary_key + name = @primary_key if name == "id" && @primary_key _read_attribute(name, &block) end # This method exists to avoid the expensive primary_key check internally, without # breaking compatibility with the read_attribute API def _read_attribute(attr_name, &block) # :nodoc + sync_with_transaction_state if @transaction_state&.finalized? @attributes.fetch_value(attr_name.to_s, &block) end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 6e0e90f39c..7bc03b9eed 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -79,7 +79,6 @@ module ActiveRecord end private - def type_incompatible_with_serialize?(type, class_name) type.is_a?(ActiveRecord::Type::Json) && class_name == ::JSON || type.respond_to?(:type_cast_array, true) && class_name == ::Array diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 294a3dc32c..fb44232dff 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -25,7 +25,6 @@ module ActiveRecord end private - def convert_time_to_time_zone(value) return if value.nil? @@ -64,7 +63,6 @@ module ActiveRecord module ClassMethods # :nodoc: private - def inherited(subclass) super # We need to apply this decorator here, rather than on module inclusion. The closure diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index d5ba2f42cb..66536a8ddf 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -11,17 +11,13 @@ module ActiveRecord module ClassMethods # :nodoc: private - def define_method_attribute=(name) - sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key - ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( generated_attribute_methods, name, writer: true, ) do |temp_method_name, attr_name_expr| generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{temp_method_name}(value) name = #{attr_name_expr} - #{sync_with_transaction_state} _write_attribute(name, value) end RUBY @@ -34,27 +30,24 @@ module ActiveRecord # turned into +nil+. def write_attribute(attr_name, value) name = attr_name.to_s - if self.class.attribute_alias?(name) - name = self.class.attribute_alias(name) - end + name = self.class.attribute_aliases[name] || name - primary_key = self.class.primary_key - name = primary_key if name == "id" && primary_key - sync_with_transaction_state if name == primary_key + name = @primary_key if name == "id" && @primary_key _write_attribute(name, value) end # This method exists to avoid the expensive primary_key check internally, without # breaking compatibility with the write_attribute API def _write_attribute(attr_name, value) # :nodoc: + sync_with_transaction_state if @transaction_state&.finalized? @attributes.write_from_user(attr_name.to_s, value) value end private def write_attribute_without_type_cast(attr_name, value) - name = attr_name.to_s - @attributes.write_cast_value(name, value) + sync_with_transaction_state if @transaction_state&.finalized? + @attributes.write_cast_value(attr_name.to_s, value) value end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 7cf421c184..c7846dbe7a 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -255,7 +255,6 @@ module ActiveRecord end private - NO_DEFAULT_PROVIDED = Object.new # :nodoc: private_constant :NO_DEFAULT_PROVIDED diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 50f29a81a6..734ebb45ae 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -147,7 +147,6 @@ module ActiveRecord module ClassMethods # :nodoc: private - def define_non_cyclic_method(name, &block) return if instance_methods(false).include?(name) define_method(name) do |*args| @@ -267,7 +266,6 @@ module ActiveRecord end private - # Returns the record for an association collection that should be validated # or saved. If +autosave+ is +false+ only new records will be returned, # unless the parent is/was a new record itself. @@ -304,7 +302,7 @@ module ActiveRecord def validate_single_association(reflection) association = association_instance_get(reflection.name) record = association && association.reader - association_valid?(reflection, record) if record + association_valid?(reflection, record) if record && record.changed_for_autosave? end # Validate the associated records if <tt>:validate</tt> or @@ -330,21 +328,16 @@ module ActiveRecord if reflection.options[:autosave] indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors) - record.errors.each do |attribute, message| + record.errors.group_by_attribute.each { |attribute, errors| attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute) - errors[attribute] << message - errors[attribute].uniq! - end - - record.errors.details.each_key do |attribute| - reflection_attribute = - normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym - record.errors.details[attribute].each do |error| - errors.details[reflection_attribute] << error - errors.details[reflection_attribute].uniq! - end - end + errors.each { |error| + self.errors.import( + error, + attribute: attribute + ) + } + } else errors.add(reflection.name) end @@ -416,7 +409,7 @@ module ActiveRecord saved = record.save(validate: false) end - raise ActiveRecord::Rollback unless saved + raise(RecordInvalid.new(association.owner)) unless saved end end end @@ -500,9 +493,7 @@ module ActiveRecord end def _ensure_no_duplicate_errors - errors.messages.each_key do |attribute| - errors[attribute].uniq! - end + errors.uniq! end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 2af6d09b53..282c9fcf30 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -12,7 +12,6 @@ require "active_support/core_ext/hash/slice" require "active_support/core_ext/string/behavior" require "active_support/core_ext/kernel/singleton_class" require "active_support/core_ext/module/introspection" -require "active_support/core_ext/object/duplicable" require "active_support/core_ext/class/subclasses" require "active_record/attribute_decorators" require "active_record/define_callbacks" diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index ef5444dfc3..a9ab9ab7a9 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -323,7 +323,6 @@ module ActiveRecord end private - def create_or_update(**) _run_save_callbacks { super } end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 11559141c7..881f0bcdb0 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -39,7 +39,6 @@ module ActiveRecord end private - def check_arity_of_constructor load(nil) rescue ArgumentError 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 68498b5dc5..36001efdd5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -3,6 +3,7 @@ require "thread" require "concurrent/map" require "monitor" +require "weakref" module ActiveRecord # Raised when a connection could not be obtained within the connection @@ -19,6 +20,26 @@ module ActiveRecord end module ConnectionAdapters + module AbstractPool # :nodoc: + def get_schema_cache(connection) + @schema_cache ||= SchemaCache.new(connection) + @schema_cache.connection = connection + @schema_cache + end + + def set_schema_cache(cache) + @schema_cache = cache + end + end + + class NullPool # :nodoc: + include ConnectionAdapters::AbstractPool + + def initialize + @schema_cache = nil + end + end + # Connection pool base class for managing Active Record database # connections. # @@ -146,7 +167,6 @@ module ActiveRecord end private - def internal_poll(timeout) no_wait_poll || (timeout && wait_poll(timeout)) end @@ -294,23 +314,50 @@ module ActiveRecord @frequency = frequency end + @mutex = Mutex.new + @pools = {} + + class << self + def register_pool(pool, frequency) # :nodoc: + @mutex.synchronize do + unless @pools.key?(frequency) + @pools[frequency] = [] + spawn_thread(frequency) + end + @pools[frequency] << WeakRef.new(pool) + end + end + + private + def spawn_thread(frequency) + Thread.new(frequency) do |t| + loop do + sleep t + @mutex.synchronize do + @pools[frequency].select!(&:weakref_alive?) + @pools[frequency].each do |p| + p.reap + p.flush + rescue WeakRef::RefError + end + end + end + end + end + end + def run return unless frequency && frequency > 0 - Thread.new(frequency, pool) { |t, p| - loop do - sleep t - p.reap - p.flush - end - } + self.class.register_pool(pool, frequency) end end include MonitorMixin include QueryCache::ConnectionPoolConfiguration + include ConnectionAdapters::AbstractPool attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache - attr_reader :spec, :connections, :size, :reaper + attr_reader :spec, :size, :reaper # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, @@ -379,7 +426,7 @@ module ActiveRecord # #connection can be called any number of times; the connection is # held in a cache keyed by a thread. def connection - @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout + @thread_cached_conns[connection_cache_key(current_thread)] ||= checkout end # Returns true if there is an open connection being used for the current thread. @@ -388,7 +435,7 @@ module ActiveRecord # #connection or #with_connection methods. Connections obtained through # #checkout will not be detected by #active_connection? def active_connection? - @thread_cached_conns[connection_cache_key(Thread.current)] + @thread_cached_conns[connection_cache_key(current_thread)] end # Signal that the thread is finished with the current connection. @@ -423,6 +470,21 @@ module ActiveRecord synchronize { @connections.any? } end + # Returns an array containing the connections currently in the pool. + # Access to the array does not require synchronization on the pool because + # the array is newly created and not retained by the pool. + # + # However; this method bypasses the ConnectionPool's thread-safe connection + # access pattern. A returned connection may be owned by another thread, + # unowned, or by happen-stance owned by the calling thread. + # + # Calling methods on a connection without ownership is subject to the + # thread-safety guarantees of the underlying method. Many of the methods + # on connection adapter classes are inherently multi-thread unsafe. + def connections + synchronize { @connections.dup } + end + # Disconnects all connections in the pool, and clears the pool. # # Raises: @@ -668,6 +730,10 @@ module ActiveRecord thread end + def current_thread + @lock_thread || Thread.current + end + # Take control of all existing connections so a "group" action such as # reload/disconnect can be performed safely. It is no longer enough to # wrap it in +synchronize+ because some pool's actions are allowed @@ -809,7 +875,6 @@ module ActiveRecord def new_connection Base.send(spec.adapter_method, spec.config).tap do |conn| - conn.schema_cache = schema_cache.dup if schema_cache conn.check_version end end @@ -938,15 +1003,30 @@ module ActiveRecord end end + attr_reader :prevent_writes + def initialize # These caches are keyed by spec.name (ConnectionSpecification#name). @owner_to_pool = ConnectionHandler.create_owner_to_pool + @prevent_writes = false # Backup finalizer: if the forked child never needed a pool, the above # early discard has not occurred ObjectSpace.define_finalizer self, ConnectionHandler.unowned_pool_finalizer(@owner_to_pool) end + # 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 connection_pool_list owner_to_pool.values.compact end @@ -1064,7 +1144,6 @@ module ActiveRecord end private - def owner_to_pool @owner_to_pool[Process.pid] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 75e959045e..d932f068f2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/deprecation" - module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseLimits 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 ef19538447..044272ea51 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -205,8 +205,6 @@ module ActiveRecord # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: # https://dev.mysql.com/doc/refman/5.7/en/savepoint.html - # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' - # supports savepoints. # # It is safe to call this method if a database transaction is already open, # i.e. if #transaction is called within another #transaction block. In case 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 a7753e3e9c..768122b4d2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -33,17 +33,17 @@ module ActiveRecord end def enable_query_cache! - @query_cache_enabled[connection_cache_key(Thread.current)] = true + @query_cache_enabled[connection_cache_key(current_thread)] = true connection.enable_query_cache! if active_connection? end def disable_query_cache! - @query_cache_enabled.delete connection_cache_key(Thread.current) + @query_cache_enabled.delete connection_cache_key(current_thread) connection.disable_query_cache! if active_connection? end def query_cache_enabled - @query_cache_enabled[connection_cache_key(Thread.current)] + @query_cache_enabled[connection_cache_key(current_thread)] end end @@ -109,7 +109,6 @@ module ActiveRecord end private - def cache_sql(sql, name, binds) @lock.synchronize do result = diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 2877530917..93273f6cf6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -114,16 +114,16 @@ module ActiveRecord # if the value is a Time responding to usec. def quoted_date(value) if value.acts_like?(:time) - zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal - - if value.respond_to?(zone_conversion_method) - value = value.send(zone_conversion_method) + if ActiveRecord::Base.default_timezone == :utc + value = value.getutc if value.respond_to?(:getutc) && !value.utc? + else + value = value.getlocal if value.respond_to?(:getlocal) end end result = value.to_s(:db) if value.respond_to?(:usec) && value.usec > 0 - "#{result}.#{sprintf("%06d", value.usec)}" + result << "." << sprintf("%06d", value.usec) else result end @@ -142,6 +142,59 @@ module ActiveRecord value.to_s.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "") end + def column_name_matcher # :nodoc: + COLUMN_NAME + end + + def column_name_with_order_matcher # :nodoc: + COLUMN_NAME_WITH_ORDER + end + + # Regexp for column names (with or without a table name prefix). + # Matches the following: + # + # "#{table_name}.#{column_name}" + # "#{column_name}" + COLUMN_NAME = / + \A + ( + (?: + # table_name.column_name | function(one or no argument) + ((?:\w+\.)?\w+) | \w+\((?:|\g<2>)\) + ) + (?:(?:\s+AS)?\s+\w+)? + ) + (?:\s*,\s*\g<1>)* + \z + /ix + + # Regexp for column names with order (with or without a table name prefix, + # with or without various order modifiers). Matches the following: + # + # "#{table_name}.#{column_name}" + # "#{table_name}.#{column_name} #{direction}" + # "#{table_name}.#{column_name} #{direction} NULLS FIRST" + # "#{table_name}.#{column_name} NULLS LAST" + # "#{column_name}" + # "#{column_name} #{direction}" + # "#{column_name} #{direction} NULLS FIRST" + # "#{column_name} NULLS LAST" + COLUMN_NAME_WITH_ORDER = / + \A + ( + (?: + # table_name.column_name | function(one or no argument) + ((?:\w+\.)?\w+) | \w+\((?:|\g<2>)\) + ) + (?:\s+ASC|\s+DESC)? + (?:\s+NULLS\s+(?:FIRST|LAST))? + ) + (?:\s*,\s*\g<1>)* + \z + /ix + + private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER + private def type_casted_binds(binds) if binds.first.is_a?(Array) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb index 52a796b926..d6dbef3fc8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb @@ -8,15 +8,15 @@ module ActiveRecord end def create_savepoint(name = current_savepoint_name) - execute("SAVEPOINT #{name}") + execute("SAVEPOINT #{name}", "TRANSACTION") end def exec_rollback_to_savepoint(name = current_savepoint_name) - execute("ROLLBACK TO SAVEPOINT #{name}") + execute("ROLLBACK TO SAVEPOINT #{name}", "TRANSACTION") end def release_savepoint(name = current_savepoint_name) - execute("RELEASE SAVEPOINT #{name}") + execute("RELEASE SAVEPOINT #{name}", "TRANSACTION") end 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 7d20825a75..23c993cfc3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -19,7 +19,6 @@ module ActiveRecord to: :@conn, private: true private - def visit_AlterTable(o) sql = +"ALTER TABLE #{quote_table_name(o.name)} " sql << o.adds.map { |col| accept col }.join(" ") 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 688eea75e8..dbd533b4b3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -264,8 +264,7 @@ module ActiveRecord if_not_exists: false, options: nil, as: nil, - comment: nil, - ** + comment: nil ) @conn = conn @columns_hash = {} diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 622e00fffb..fb56e712be 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -15,7 +15,7 @@ module ActiveRecord def column_spec_for_primary_key(column) return {} if default_primary_key?(column) spec = { id: schema_type(column).inspect } - spec.merge!(prepare_column_options(column).except!(:null)) + spec.merge!(prepare_column_options(column).except!(:null, :comment)) spec[:default] ||= "nil" if explicit_primary_key_default?(column) spec end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 7041d92586..88367c79a1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -2,7 +2,6 @@ require "active_record/migration/join_table" require "active_support/core_ext/string/access" -require "active_support/deprecation" require "digest/sha2" module ActiveRecord @@ -101,7 +100,7 @@ module ActiveRecord def index_exists?(table_name, column_name, options = {}) column_names = Array(column_name).map(&:to_s) checks = [] - checks << lambda { |i| i.columns == column_names } + checks << lambda { |i| Array(i.columns) == column_names } checks << lambda { |i| i.unique } if options[:unique] checks << lambda { |i| i.name == options[:name].to_s } if options[:name] @@ -291,25 +290,27 @@ 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, **options) - td = create_table_definition(table_name, options) + def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options) + td = create_table_definition( + table_name, options.extract!(:temporary, :if_not_exists, :options, :as, :comment) + ) - if options[:id] != false && !options[:as] - pk = options.fetch(:primary_key) do - Base.get_primary_key table_name.to_s.singularize - end + if id && !td.as + pk = primary_key || Base.get_primary_key(table_name.to_s.singularize) if pk.is_a?(Array) td.primary_keys pk else - td.primary_key pk, options.fetch(:id, :primary_key), options + td.primary_key pk, id, options end end yield td if block_given? - if options[:force] - drop_table(table_name, options.merge(if_exists: true)) + if force + drop_table(table_name, force: force, if_exists: true) + else + schema_cache.clear_data_source_cache!(table_name.to_s) end result = execute schema_creation.accept td @@ -321,7 +322,7 @@ module ActiveRecord end if supports_comments? && !supports_comments_in_create? - if table_comment = options[:comment].presence + if table_comment = td.comment.presence change_table_comment(table_name, table_comment) end @@ -499,6 +500,7 @@ module ActiveRecord # it can be helpful to provide these in a migration's +change+ method so it can be reverted. # In that case, +options+ and the block will be used by #create_table. def drop_table(table_name, options = {}) + schema_cache.clear_data_source_cache!(table_name.to_s) execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}" end @@ -518,14 +520,15 @@ module ActiveRecord # Available options are (none of these exists by default): # * <tt>:limit</tt> - # Requests a maximum column length. This is the number of characters for a <tt>:string</tt> column - # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns. + # and number of bytes for <tt>:text</tt>, <tt>:binary</tt>, and <tt>:integer</tt> columns. # This option is ignored by some backends. # * <tt>:default</tt> - # The column's default value. Use +nil+ for +NULL+. # * <tt>:null</tt> - # Allows or disallows +NULL+ values in the column. # * <tt>:precision</tt> - - # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. + # Specifies the precision for the <tt>:decimal</tt>, <tt>:numeric</tt>, + # <tt>:datetime</tt>, and <tt>:time</tt> columns. # * <tt>:scale</tt> - # Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # * <tt>:collation</tt> - @@ -735,7 +738,7 @@ module ActiveRecord # # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active # - # Note: Partial indexes are only supported for PostgreSQL and SQLite 3.8.0+. + # Note: Partial indexes are only supported for PostgreSQL and SQLite. # # ====== Creating an index with a specific method # @@ -770,6 +773,17 @@ module ActiveRecord # CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL # # Note: only supported by MySQL. + # + # ====== Creating an index with a specific algorithm + # + # add_index(:developers, :name, algorithm: :concurrently) + # # CREATE INDEX CONCURRENTLY developers_on_name on developers (name) + # + # Note: only supported by PostgreSQL. + # + # Concurrently adding an index is not supported in a transaction. + # + # For more information see the {"Transactional Migrations" section}[rdoc-ref:Migration]. def add_index(table_name, column_name, options = {}) index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options) execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}" @@ -793,6 +807,15 @@ module ActiveRecord # # remove_index :accounts, name: :by_branch_party # + # Removes the index named +by_branch_party+ in the +accounts+ table +concurrently+. + # + # remove_index :accounts, name: :by_branch_party, algorithm: :concurrently + # + # Note: only supported by PostgreSQL. + # + # Concurrently removing an index is not supported in a transaction. + # + # For more information see the {"Transactional Migrations" section}[rdoc-ref:Migration]. def remove_index(table_name, options = {}) index_name = index_name_for_remove(table_name, options) execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" @@ -1040,8 +1063,8 @@ module ActiveRecord options end - def dump_schema_information #:nodoc: - versions = ActiveRecord::SchemaMigration.all_versions + def dump_schema_information # :nodoc: + versions = schema_migration.all_versions insert_versions_sql(versions) if versions.any? end @@ -1057,7 +1080,7 @@ module ActiveRecord end version = version.to_i - sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) + sm_table = quote_table_name(schema_migration.table_name) migrated = migration_context.get_all_versions versions = migration_context.migrations.map(&:version) @@ -1430,7 +1453,7 @@ module ActiveRecord end def insert_versions_sql(versions) - sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) + sm_table = quote_table_name(schema_migration.table_name) if versions.is_a?(Array) sql = +"INSERT INTO #{sm_table} (version) VALUES\n" diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index c9e84e48cc..53ce8df491 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -5,10 +5,11 @@ module ActiveRecord class TransactionState def initialize(state = nil) @state = state - @children = [] + @children = nil end def add_child(state) + @children ||= [] @children << state end @@ -41,12 +42,12 @@ module ActiveRecord end def rollback! - @children.each { |c| c.rollback! } + @children&.each { |c| c.rollback! } @state = :rolledback end def full_rollback! - @children.each { |c| c.rollback! } + @children&.each { |c| c.rollback! } @state = :fully_rolledback end @@ -75,18 +76,19 @@ module ActiveRecord class Transaction #:nodoc: attr_reader :connection, :state, :records, :savepoint_name, :isolation_level - def initialize(connection, options, run_commit_callbacks: false) + def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false) @connection = connection @state = TransactionState.new - @records = [] - @isolation_level = options[:isolation] + @records = nil + @isolation_level = isolation @materialized = false - @joinable = options.fetch(:joinable, true) + @joinable = joinable @run_commit_callbacks = run_commit_callbacks end def add_record(record) - records << record + @records ||= [] + @records << record end def materialize! @@ -98,32 +100,42 @@ module ActiveRecord end def rollback_records - ite = records.uniq + return unless records + ite = records.uniq(&:object_id) + already_run_callbacks = {} while record = ite.shift - record.rolledback!(force_restore_state: full_rollback?) + trigger_callbacks = record.trigger_transactional_callbacks? + should_run_callbacks = !already_run_callbacks[record] && trigger_callbacks + already_run_callbacks[record] ||= trigger_callbacks + record.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: should_run_callbacks) end ensure - ite.each do |i| + ite&.each do |i| i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false) end end def before_commit_records - records.uniq.each(&:before_committed!) if @run_commit_callbacks + records.uniq.each(&:before_committed!) if records && @run_commit_callbacks end def commit_records - ite = records.uniq + return unless records + ite = records.uniq(&:object_id) + already_run_callbacks = {} while record = ite.shift if @run_commit_callbacks - record.committed! + trigger_callbacks = record.trigger_transactional_callbacks? + should_run_callbacks = !already_run_callbacks[record] && trigger_callbacks + already_run_callbacks[record] ||= trigger_callbacks + record.committed!(should_run_callbacks: should_run_callbacks) else # if not running callbacks, only adds the record to the parent transaction connection.add_transaction_record(record) end end ensure - ite.each { |i| i.committed!(should_run_callbacks: false) } + ite&.each { |i| i.committed!(should_run_callbacks: false) } end def full_rollback?; true; end @@ -133,8 +145,8 @@ module ActiveRecord end class SavepointTransaction < Transaction - def initialize(connection, savepoint_name, parent_transaction, *args) - super(connection, *args) + def initialize(connection, savepoint_name, parent_transaction, **options) + super(connection, options) parent_transaction.state.add_child(@state) @@ -194,18 +206,29 @@ module ActiveRecord @lazy_transactions_enabled = true end - def begin_transaction(options = {}) + def begin_transaction(isolation: nil, joinable: true, _lazy: true) @connection.lock.synchronize do run_commit_callbacks = !current_transaction.joinable? transaction = if @stack.empty? - RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) + RealTransaction.new( + @connection, + isolation: isolation, + joinable: joinable, + run_commit_callbacks: run_commit_callbacks + ) else - SavepointTransaction.new(@connection, "active_record_#{@stack.size}", @stack.last, options, - run_commit_callbacks: run_commit_callbacks) + SavepointTransaction.new( + @connection, + "active_record_#{@stack.size}", + @stack.last, + isolation: isolation, + joinable: joinable, + run_commit_callbacks: run_commit_callbacks + ) end - if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && options[:_lazy] != false + if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && _lazy @has_unmaterialized_transactions = true else transaction.materialize! @@ -266,9 +289,9 @@ module ActiveRecord end end - def within_new_transaction(options = {}) + def within_new_transaction(isolation: nil, joinable: true) @connection.lock.synchronize do - transaction = begin_transaction options + transaction = begin_transaction(isolation: isolation, joinable: joinable) yield rescue Exception => error if transaction @@ -301,7 +324,6 @@ module ActiveRecord end private - NULL_TRANSACTION = NullTransaction.new # Deallocate invalidated prepared statements outside of the transaction diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 200184c2f9..dc970c384b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -6,7 +6,6 @@ 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" @@ -78,7 +77,7 @@ module ActiveRecord SIMPLE_INT = /\A\d+\z/ attr_accessor :pool - attr_reader :schema_cache, :visitor, :owner, :logger, :lock, :prepared_statements, :prevent_writes + attr_reader :visitor, :owner, :logger, :lock, :prepared_statements alias :in_use? :owner set_callback :checkin, :after, :enable_lazy_transactions! @@ -106,6 +105,14 @@ module ActiveRecord Regexp.union(*parts) end + def self.quoted_column_names # :nodoc: + @quoted_column_names ||= {} + end + + def self.quoted_table_names # :nodoc: + @quoted_table_names ||= {} + end + def initialize(connection, logger = nil, config = {}) # :nodoc: super() @@ -114,11 +121,8 @@ module ActiveRecord @instrumenter = ActiveSupport::Notifications.instrumenter @logger = logger @config = config - @pool = nil + @pool = ActiveRecord::ConnectionAdapters::NullPool.new @idle_since = Concurrent.monotonic_time - @schema_cache = SchemaCache.new self - @quoted_column_names, @quoted_table_names = {}, {} - @prevent_writes = false @visitor = arel_visitor @statements = build_statement_pool @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new @@ -144,19 +148,7 @@ module ActiveRecord # 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 + replica? || ActiveRecord::Base.connection_handler.prevent_writes end def migrations_paths # :nodoc: @@ -164,14 +156,32 @@ module ActiveRecord end def migration_context # :nodoc: - MigrationContext.new(migrations_paths) + MigrationContext.new(migrations_paths, schema_migration) + end + + def schema_migration # :nodoc: + @schema_migration ||= begin + conn = self + spec_name = conn.pool.spec.name + name = "#{spec_name}::SchemaMigration" + + Class.new(ActiveRecord::SchemaMigration) do + define_singleton_method(:name) { name } + define_singleton_method(:to_s) { name } + + self.connection_specification_name = spec_name + end + end end class Version include Comparable - def initialize(version_string) + attr_reader :full_version_string + + def initialize(version_string, full_version_string = nil) @version = version_string.split(".").map(&:to_i) + @full_version_string = full_version_string end def <=>(version_string) @@ -203,9 +213,13 @@ module ActiveRecord @owner = Thread.current end + def schema_cache + @pool.get_schema_cache(self) + end + def schema_cache=(cache) cache.connection = self - @schema_cache = cache + @pool.set_schema_cache(cache) end # this method must only be called while holding connection pool's mutex @@ -256,6 +270,11 @@ module ActiveRecord self.class::ADAPTER_NAME end + # Does the database for this adapter exist? + def self.database_exists?(config) + raise NotImplementedError + end + # Does this adapter support DDL rollbacks in transactions? That is, would # CREATE TABLE or ALTER TABLE get rolled back by a transaction? def supports_ddl_transactions? @@ -484,6 +503,9 @@ module ActiveRecord # # Prevent @connection's finalizer from touching the socket, or # otherwise communicating with its server, when it is collected. + if schema_cache.connection == self + schema_cache.connection = nil + end end # Reset the state of this connection, directing the DBMS to clear @@ -584,7 +606,6 @@ module ActiveRecord end private - def type_map @type_map ||= Type::TypeMap.new.tap do |mapping| initialize_type_map(mapping) 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 8b907759c6..ef1eef6b69 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -45,7 +45,6 @@ module ActiveRecord class StatementPool < ConnectionAdapters::StatementPool # :nodoc: private - def dealloc(stmt) stmt.close end @@ -56,7 +55,9 @@ module ActiveRecord end def get_database_version #:nodoc: - Version.new(version_string) + full_version_string = get_full_version + version_string = version_string(full_version_string) + Version.new(version_string, full_version_string) end def mariadb? # :nodoc: @@ -174,15 +175,6 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== #++ - def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel, binds)}" - start = Concurrent.monotonic_time - result = exec_query(sql, "EXPLAIN", binds) - elapsed = Concurrent.monotonic_time - start - - MySQL::ExplainPrettyPrinter.new.pp(result, elapsed) - end - # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) materialize_transactions @@ -202,7 +194,7 @@ module ActiveRecord end def begin_db_transaction - execute "BEGIN" + execute("BEGIN", "TRANSACTION") end def begin_isolated_db_transaction(isolation) @@ -211,11 +203,11 @@ module ActiveRecord end def commit_db_transaction #:nodoc: - execute "COMMIT" + execute("COMMIT", "TRANSACTION") end def exec_rollback_db_transaction #:nodoc: - execute "ROLLBACK" + execute("ROLLBACK", "TRANSACTION") end def empty_insert_statement_value(primary_key = nil) @@ -296,6 +288,8 @@ module ActiveRecord # Example: # rename_table('octopuses', 'octopi') def rename_table(table_name, new_name) + schema_cache.clear_data_source_cache!(table_name.to_s) + schema_cache.clear_data_source_cache!(new_name.to_s) execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" rename_table_indexes(table_name, new_name) end @@ -316,6 +310,7 @@ module ActiveRecord # it can be helpful to provide these in a migration's +change+ method so it can be reverted. # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) + schema_cache.clear_data_source_cache!(table_name.to_s) execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end @@ -481,12 +476,12 @@ module ActiveRecord # distinct queries, and requires that the ORDER BY include the distinct column. # See https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html def columns_for_distinct(columns, orders) # :nodoc: - order_columns = orders.reject(&:blank?).map { |s| + order_columns = orders.compact_blank.map { |s| # Convert Arel node to string s = s.to_sql unless s.is_a?(String) # Remove any ASC/DESC modifiers s.gsub(/\s+(?:ASC|DESC)\b/i, "") - }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } + }.compact_blank.map.with_index { |column, i| "#{column} AS alias_#{i}" } (order_columns << super).join(", ") end @@ -520,7 +515,6 @@ module ActiveRecord end private - def initialize_type_map(m = type_map) super @@ -625,7 +619,11 @@ module ActiveRecord when ER_QUERY_INTERRUPTED QueryCanceled.new(message, sql: sql, binds: binds) else - super + if exception.is_a?(Mysql2::Error::TimeoutError) + ActiveRecord::AdapterTimeout.new(message, sql: sql, binds: binds) + else + super + end end end @@ -743,7 +741,7 @@ module ActiveRecord end.compact.join(", ") # ...and send them all in one query - execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}" + execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}", "SCHEMA") end def column_definitions(table_name) # :nodoc: @@ -788,8 +786,8 @@ module ActiveRecord MismatchedForeignKey.new(options) end - def version_string - full_version.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1] + def version_string(full_version_string) + full_version_string.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1] end class MysqlString < Type::String # :nodoc: @@ -802,7 +800,6 @@ module ActiveRecord end private - def cast_value(value) case value when true then "1" diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 279d0b9e84..2708d2756b 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -5,6 +5,8 @@ module ActiveRecord module ConnectionAdapters # An abstract definition of a column in a table. class Column + include Deduplicable + attr_reader :name, :default, :sql_type_metadata, :null, :default_function, :collation, :comment delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true @@ -76,6 +78,7 @@ module ActiveRecord def hash Column.hash ^ name.hash ^ + name.encoding.hash ^ default.hash ^ sql_type_metadata.hash ^ null.hash ^ @@ -83,6 +86,17 @@ module ActiveRecord collation.hash ^ comment.hash end + + private + def deduplicated + @name = -name + @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata + @default = -default if default + @default_function = -default_function if default_function + @collation = -collation if collation + @comment = -comment if comment + super + end end class NullColumn < Column diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 9eaf9d9a89..0732b69f81 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -50,13 +50,12 @@ module ActiveRecord # Converts the given URL to a full connection hash. def to_hash - config = raw_config.reject { |_, value| value.blank? } + config = raw_config.compact_blank config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String } config end private - attr_reader :uri def uri_parser diff --git a/activerecord/lib/active_record/connection_adapters/deduplicable.rb b/activerecord/lib/active_record/connection_adapters/deduplicable.rb new file mode 100644 index 0000000000..fb2fd60bbc --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/deduplicable.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module ActiveRecord + module ConnectionAdapters # :nodoc: + module Deduplicable + extend ActiveSupport::Concern + + module ClassMethods + def registry + @registry ||= {} + end + + def new(*) + super.deduplicate + end + end + + def deduplicate + self.class.registry[self] ||= deduplicated + end + alias :-@ :deduplicate + + private + def deduplicated + freeze + end + end + end +end 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 1df4dea2d8..97d74df529 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 @@ -5,7 +5,7 @@ module ActiveRecord module DetermineIfPreparableVisitor attr_accessor :preparable - def accept(*) + def accept(object, collector) @preparable = true super end @@ -20,7 +20,7 @@ module ActiveRecord super end - def visit_Arel_Nodes_SqlLiteral(*) + def visit_Arel_Nodes_SqlLiteral(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 2132e5d248..bbcdc96cdc 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -26,6 +26,15 @@ module ActiveRecord !READ_QUERY.match?(sql) end + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel, binds)}" + start = Concurrent.monotonic_time + result = exec_query(sql, "EXPLAIN", binds) + elapsed = Concurrent.monotonic_time - start + + MySQL::ExplainPrettyPrinter.new.pp(result, elapsed) + end + # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) if preventing_writes? && write_query?(sql) diff --git a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb index 20c3c83664..edd5ea0542 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb @@ -37,7 +37,6 @@ module ActiveRecord end private - def compute_column_widths(result) [].tap do |widths| result.columns.each_with_index do |column, i| diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb index 75564a61d6..0069f5871c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -5,11 +5,11 @@ module ActiveRecord module MySQL module Quoting # :nodoc: def quote_column_name(name) - @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`" + self.class.quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`" end def quote_table_name(name) - @quoted_table_names[name] ||= super.gsub(".", "`.`").freeze + self.class.quoted_table_names[name] ||= super.gsub(".", "`.`").freeze end def unquoted_true @@ -32,12 +32,49 @@ module ActiveRecord "x'#{value.hex}'" end - def _type_cast(value) - case value - when Date, Time then value - else super - end + def column_name_matcher + COLUMN_NAME + end + + def column_name_with_order_matcher + COLUMN_NAME_WITH_ORDER end + + COLUMN_NAME = / + \A + ( + (?: + # `table_name`.`column_name` | function(one or no argument) + ((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`)) | \w+\((?:|\g<2>)\) + ) + (?:(?:\s+AS)?\s+(?:\w+|`\w+`))? + ) + (?:\s*,\s*\g<1>)* + \z + /ix + + COLUMN_NAME_WITH_ORDER = / + \A + ( + (?: + # `table_name`.`column_name` | function(one or no argument) + ((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`)) | \w+\((?:|\g<2>)\) + ) + (?:\s+ASC|\s+DESC)? + ) + (?:\s*,\s*\g<1>)* + \z + /ix + + private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER + + private + def _type_cast(value) + case value + when Date, Time then value + else super + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index 82ed320617..0f5ab7562a 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -7,7 +7,6 @@ module ActiveRecord delegate :add_sql_comment!, :mariadb?, to: :@conn, private: true private - def visit_DropForeignKey(name) "DROP FOREIGN KEY #{name}" 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 234fb25fdf..bcd300f3db 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -41,13 +41,15 @@ module ActiveRecord case column.sql_type when /\Atimestamp\b/ :timestamp + when /\A(?:enum|set)\b/ + column.sql_type else super end end def schema_limit(column) - super unless /\A(?:tiny|medium|long)?(?:text|blob)/.match?(column.sql_type) + super unless /\A(?:enum|set|(?:tiny|medium|long)?(?:text|blob))\b/.match?(column.sql_type) end def schema_precision(column) diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb index 9167593064..a7232fa249 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb @@ -6,9 +6,11 @@ module ActiveRecord class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: undef to_yaml if method_defined?(:to_yaml) + include Deduplicable + attr_reader :extra - def initialize(type_metadata, extra: "") + def initialize(type_metadata, extra: nil) super(type_metadata) @extra = extra end @@ -25,6 +27,13 @@ module ActiveRecord __getobj__.hash ^ extra.hash end + + private + def deduplicated + __setobj__(__getobj__.deduplicate) + @extra = -extra if extra + super + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 0dc880c731..1df9ac32c9 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -8,6 +8,8 @@ require "mysql2" module ActiveRecord module ConnectionHandling # :nodoc: + ER_BAD_DB_ERROR = 1049 + # Establishes a connection to the database that's used by all Active Record objects. def mysql2_connection(config) config = config.symbolize_keys @@ -22,7 +24,7 @@ module ActiveRecord client = Mysql2::Client.new(config) ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config) rescue Mysql2::Error => error - if error.message.include?("Unknown database") + if error.error_number == ER_BAD_DB_ERROR raise ActiveRecord::NoDatabaseError else raise @@ -42,6 +44,12 @@ module ActiveRecord configure_connection end + def self.database_exists?(config) + !!ActiveRecord::Base.mysql2_connection(config) + rescue ActiveRecord::NoDatabaseError + false + end + def supports_json? !mariadb? && database_version >= "5.7.8" end @@ -109,12 +117,12 @@ module ActiveRecord end def discard! # :nodoc: + super @connection.automatic_close = false @connection = nil end private - def connect @connection = Mysql2::Client.new(@config) configure_connection @@ -126,7 +134,11 @@ module ActiveRecord end def full_version - @full_version ||= @connection.server_info[:version] + schema_cache.database_version.full_version_string + end + + def get_full_version + @connection.server_info[:version] end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index ec25bb1e19..f1ecf6df30 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -23,6 +23,29 @@ module ActiveRecord def sql_type super.sub(/\[\]\z/, "") end + + def init_with(coder) + @serial = coder["serial"] + super + end + + def encode_with(coder) + coder["serial"] = @serial + super + end + + def ==(other) + other.is_a?(Column) && + super && + serial? == other.serial? + end + alias :eql? :== + + def hash + Column.hash ^ + super.hash ^ + serial?.hash + end end end PostgreSQLColumn = PostgreSQL::Column # :nodoc: 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 d872bd662f..45ec79ca78 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -145,7 +145,7 @@ module ActiveRecord # Begins a transaction. def begin_db_transaction - execute "BEGIN" + execute("BEGIN", "TRANSACTION") end def begin_isolated_db_transaction(isolation) @@ -155,12 +155,12 @@ module ActiveRecord # Commits a transaction. def commit_db_transaction - execute "COMMIT" + execute("COMMIT", "TRANSACTION") end # Aborts a transaction. def exec_rollback_db_transaction - execute "ROLLBACK" + execute("ROLLBACK", "TRANSACTION") end private 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 b1dfbde86e..0bbe98145a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -77,7 +77,6 @@ module ActiveRecord end private - def type_cast_array(value, method) if value.is_a?(::Array) value.map { |item| type_cast_array(item, method) } diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb index f70f09ad95..bae34472e1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -10,7 +10,6 @@ module ActiveRecord end private - def cast_value(value) value.to_s end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index 7b42677101..8d4dacbd64 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -46,7 +46,6 @@ module ActiveRecord end private - HstorePair = begin quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ 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 7f6adc351c..e52d4385ef 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 @@ -34,7 +34,6 @@ module ActiveRecord end private - def number_for_point(number) number.to_s.gsub(/\.0$/, "") end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index 6434377b57..357493dfc0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -26,9 +26,9 @@ module ActiveRecord value = value.sub(/^\((.+)\)$/, '-\1') # (4) case value - when /^-?\D+[\d,]+\.\d{2}$/ # (1) + when /^-?\D*[\d,]+\.\d{2}$/ # (1) value.gsub!(/[^-\d.]/, "") - when /^-?\D+[\d.]+,\d{2}$/ # (2) + when /^-?\D*[\d.]+,\d{2}$/ # (2) value.gsub!(/[^-\d,]/, "").sub!(/,/, ".") end 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 8c74cecc4d..e81e18ff70 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -50,7 +50,6 @@ module ActiveRecord end private - def number_for_point(number) number.to_s.gsub(/\.0$/, "") end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index aa7701e038..d19f1f9cf8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -58,7 +58,6 @@ module ActiveRecord end private - def type_cast_single(value) infinity?(value) ? value : @subtype.deserialize(value) end 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 28abdbd073..74a28eef58 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -14,7 +14,6 @@ module ActiveRecord end private - def cast_value(value) casted = value.to_s casted if casted.match?(ACCEPTABLE_UUID) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index d40e0ef1f0..07b66de366 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -30,7 +30,7 @@ module ActiveRecord # - "schema.name".table_name # - "schema.name"."table.name" def quote_table_name(name) # :nodoc: - @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze + self.class.quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze end # Quotes schema names for use in SQL queries. @@ -44,7 +44,7 @@ module ActiveRecord # Quotes column names for use in SQL queries. def quote_column_name(name) # :nodoc: - @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze + self.class.quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze end # Quote date/time values for use in SQL input. @@ -78,6 +78,43 @@ module ActiveRecord type_map.lookup(column.oid, column.fmod, column.sql_type) end + def column_name_matcher + COLUMN_NAME + end + + def column_name_with_order_matcher + COLUMN_NAME_WITH_ORDER + end + + COLUMN_NAME = / + \A + ( + (?: + # "table_name"."column_name"::type_name | function(one or no argument)::type_name + ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)? + ) + (?:(?:\s+AS)?\s+(?:\w+|"\w+"))? + ) + (?:\s*,\s*\g<1>)* + \z + /ix + + COLUMN_NAME_WITH_ORDER = / + \A + ( + (?: + # "table_name"."column_name"::type_name | function(one or no argument)::type_name + ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)? + ) + (?:\s+ASC|\s+DESC)? + (?:\s+NULLS\s+(?:FIRST|LAST))? + ) + (?:\s*,\s*\g<1>)* + \z + /ix + + private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER + private def lookup_cast_type(sql_type) super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb index 84643d20da..d201e40190 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -5,7 +5,6 @@ module ActiveRecord module PostgreSQL class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc: private - def extensions(stream) extensions = @connection.extensions if extensions.any? 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 40c5e51d92..628a609521 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -55,6 +55,7 @@ module ActiveRecord end def drop_table(table_name, options = {}) # :nodoc: + schema_cache.clear_data_source_cache!(table_name.to_s) execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end @@ -376,6 +377,8 @@ module ActiveRecord # rename_table('octopuses', 'octopi') def rename_table(table_name, new_name) clear_cache! + schema_cache.clear_data_source_cache!(table_name.to_s) + schema_cache.clear_data_source_cache!(new_name.to_s) execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) if pk @@ -552,13 +555,13 @@ module ActiveRecord # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and # requires that the ORDER BY include the distinct column. def columns_for_distinct(columns, orders) #:nodoc: - order_columns = orders.reject(&:blank?).map { |s| + order_columns = orders.compact_blank.map { |s| # Convert Arel node to string s = s.to_sql unless s.is_a?(String) # Remove any ASC/DESC modifiers s.gsub(/\s+(?:ASC|DESC)\b/i, "") .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "") - }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } + }.compact_blank.map.with_index { |column, i| "#{column} AS alias_#{i}" } (order_columns << super).join(", ") end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index 8bdec623af..b7f6479357 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -7,6 +7,8 @@ module ActiveRecord class TypeMetadata < DelegateClass(SqlTypeMetadata) undef to_yaml if method_defined?(:to_yaml) + include Deduplicable + attr_reader :oid, :fmod def initialize(type_metadata, oid: nil, fmod: nil) @@ -29,6 +31,12 @@ module ActiveRecord oid.hash ^ fmod.hash end + + private + def deduplicated + __setobj__(__getobj__.deduplicate) + super + end end end PostgreSQLTypeMetadata = PostgreSQL::TypeMetadata diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb index f2f4701500..e8caeb8132 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -37,7 +37,6 @@ module ActiveRecord end protected - def parts @parts ||= [@schema, @identifier].compact end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 91318a0af1..0a7c6d8ac4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -46,7 +46,7 @@ module ActiveRecord conn = PG.connect(conn_params) ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config) rescue ::PG::Error => error - if error.message.include?("does not exist") + if error.message.include?(conn_params[:dbname]) raise ActiveRecord::NoDatabaseError else raise @@ -259,6 +259,12 @@ module ActiveRecord @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true end + def self.database_exists?(config) + !!ActiveRecord::Base.postgresql_connection(config) + rescue ActiveRecord::NoDatabaseError + false + end + # Is this connection alive and ready for queries? def active? @lock.synchronize do @@ -302,6 +308,7 @@ module ActiveRecord end def discard! # :nodoc: + super @connection.socket_io.reopen(IO::NULL) rescue nil @connection = nil end @@ -452,7 +459,6 @@ module ActiveRecord end private - # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html VALUE_LIMIT_VIOLATION = "22001" NUMERIC_VALUE_OUT_OF_RANGE = "22003" diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index dbfe1e4a34..5e30304864 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -27,7 +27,6 @@ module ActiveRecord def encode_with(coder) coder["columns"] = @columns - coder["columns_hash"] = @columns_hash coder["primary_keys"] = @primary_keys coder["data_sources"] = @data_sources coder["indexes"] = @indexes @@ -37,16 +36,21 @@ module ActiveRecord def init_with(coder) @columns = coder["columns"] - @columns_hash = coder["columns_hash"] @primary_keys = coder["primary_keys"] @data_sources = coder["data_sources"] @indexes = coder["indexes"] || {} @version = coder["version"] @database_version = coder["database_version"] + + derive_columns_hash_and_deduplicate_values end def primary_keys(table_name) - @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil + @primary_keys.fetch(table_name) do + if data_source_exists?(table_name) + @primary_keys[deep_deduplicate(table_name)] = deep_deduplicate(connection.primary_key(table_name)) + end + end end # A cached lookup for table existence. @@ -54,7 +58,7 @@ module ActiveRecord prepare_data_sources if @data_sources.empty? return @data_sources[name] if @data_sources.key? name - @data_sources[name] = connection.data_source_exists?(name) + @data_sources[deep_deduplicate(name)] = connection.data_source_exists?(name) end # Add internal cache for table with +table_name+. @@ -73,15 +77,17 @@ module ActiveRecord # Get the columns for a table def columns(table_name) - @columns[table_name] ||= connection.columns(table_name) + @columns.fetch(table_name) do + @columns[deep_deduplicate(table_name)] = deep_deduplicate(connection.columns(table_name)) + end end # Get the columns for a table as a hash, key is the column name # value is the column object. def columns_hash(table_name) - @columns_hash[table_name] ||= Hash[columns(table_name).map { |col| - [col.name, col] - }] + @columns_hash.fetch(table_name) do + @columns_hash[deep_deduplicate(table_name)] = columns(table_name).index_by(&:name) + end end # Checks whether the columns hash is already cached for a table. @@ -90,7 +96,9 @@ module ActiveRecord end def indexes(table_name) - @indexes[table_name] ||= connection.indexes(table_name) + @indexes.fetch(table_name) do + @indexes[deep_deduplicate(table_name)] = deep_deduplicate(connection.indexes(table_name)) + end end def database_version # :nodoc: @@ -124,15 +132,38 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. @version = connection.migration_context.current_version - [@version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes, database_version] + [@version, @columns, {}, @primary_keys, @data_sources, @indexes, database_version] end def marshal_load(array) - @version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes, @database_version = array - @indexes = @indexes || {} + @version, @columns, _columns_hash, @primary_keys, @data_sources, @indexes, @database_version = array + @indexes ||= {} + + derive_columns_hash_and_deduplicate_values end private + def derive_columns_hash_and_deduplicate_values + @columns = deep_deduplicate(@columns) + @columns_hash = @columns.transform_values { |columns| columns.index_by(&:name) } + @primary_keys = deep_deduplicate(@primary_keys) + @data_sources = deep_deduplicate(@data_sources) + @indexes = deep_deduplicate(@indexes) + end + + def deep_deduplicate(value) + case value + when Hash + value.transform_keys { |k| deep_deduplicate(k) }.transform_values { |v| deep_deduplicate(v) } + when Array + value.map { |i| deep_deduplicate(i) } + when String, Deduplicable + -value + else + value + end + end + def prepare_data_sources connection.data_sources.each { |source| @data_sources[source] = true } end diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb index df28df7a7c..969867e70f 100644 --- a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true +require "active_record/connection_adapters/deduplicable" + module ActiveRecord # :stopdoc: module ConnectionAdapters class SqlTypeMetadata + include Deduplicable + attr_reader :sql_type, :type, :limit, :precision, :scale def initialize(sql_type: nil, type: nil, limit: nil, precision: nil, scale: nil) @@ -32,6 +36,12 @@ module ActiveRecord precision.hash >> 1 ^ scale.hash >> 2 end + + private + def deduplicated + @sql_type = -sql_type + super + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb index 46ce1a15b5..85053acf91 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb @@ -11,6 +11,11 @@ module ActiveRecord !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 execute(sql, name = nil) #:nodoc: if preventing_writes? && write_query?(sql) raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}" @@ -68,18 +73,17 @@ module ActiveRecord alias :exec_update :exec_delete def begin_db_transaction #:nodoc: - log("begin transaction", nil) { @connection.transaction } + log("begin transaction", "TRANSACTION") { @connection.transaction } end def commit_db_transaction #:nodoc: - log("commit transaction", nil) { @connection.commit } + log("commit transaction", "TRANSACTION") { @connection.commit } end def exec_rollback_db_transaction #:nodoc: - log("rollback transaction", nil) { @connection.rollback } + log("rollback transaction", "TRANSACTION") { @connection.rollback } end - private def execute_batch(sql, name = nil) if preventing_writes? && write_query?(sql) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index cb9d32a577..9b74a774e5 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -13,11 +13,11 @@ module ActiveRecord end def quote_table_name(name) - @quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze + self.class.quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze end def quote_column_name(name) - @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}") + self.class.quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}") end def quoted_time(value) @@ -45,8 +45,43 @@ module ActiveRecord 0 end - private + def column_name_matcher + COLUMN_NAME + end + + def column_name_with_order_matcher + COLUMN_NAME_WITH_ORDER + end + COLUMN_NAME = / + \A + ( + (?: + # "table_name"."column_name" | function(one or no argument) + ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")) | \w+\((?:|\g<2>)\) + ) + (?:(?:\s+AS)?\s+(?:\w+|"\w+"))? + ) + (?:\s*,\s*\g<1>)* + \z + /ix + + COLUMN_NAME_WITH_ORDER = / + \A + ( + (?: + # "table_name"."column_name" | function(one or no argument) + ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")) | \w+\((?:|\g<2>)\) + ) + (?:\s+ASC|\s+DESC)? + ) + (?:\s*,\s*\g<1>)* + \z + /ix + + private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER + + private def _type_cast(value) case value when BigDecimal diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index f5f5827d04..f4847eb6c0 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -48,8 +48,8 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - # The SQLite3 adapter works SQLite 3.6.16 or newer - # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). + # The SQLite3 adapter works with the sqlite3-ruby drivers + # (available as gem from https://rubygems.org/gems/sqlite3). # # Options: # @@ -98,6 +98,16 @@ module ActiveRecord configure_connection end + def self.database_exists?(config) + config = config.symbolize_keys + if config[:database] == ":memory:" + return true + else + database_file = defined?(Rails.root) ? File.expand_path(config[:database], Rails.root) : config[:database] + File.exist?(database_file) + end + end + def supports_ddl_transactions? true end @@ -201,14 +211,6 @@ module ActiveRecord end end - #-- - # DATABASE STATEMENTS ====================================== - #++ - def explain(arel, binds = []) - sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" - SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", [])) - end - # SCHEMA STATEMENTS ======================================== def primary_keys(table_name) # :nodoc: @@ -226,6 +228,8 @@ module ActiveRecord # Example: # rename_table('octopuses', 'octopi') def rename_table(table_name, new_name) + schema_cache.clear_data_source_cache!(table_name.to_s) + schema_cache.clear_data_source_cache!(new_name.to_s) exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" rename_table_indexes(table_name, new_name) end @@ -389,6 +393,7 @@ module ActiveRecord if from_primary_key.is_a?(Array) @definition.primary_keys from_primary_key end + columns(from).each do |column| column_name = options[:rename] ? (options[:rename][column.name] || @@ -485,9 +490,9 @@ module ActiveRecord result = exec_query(sql, "SCHEMA").first if result - # Splitting with left parentheses and picking up last will return all + # Splitting with left parentheses and discarding the first part will return all # columns separated with comma(,). - columns_string = result["sql"].split("(").last + columns_string = result["sql"].split("(", 2).last columns_string.split(",").each do |column_string| # This regex will match the column name and collation type and will save diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb index 46bd831da7..0960feed84 100644 --- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -48,7 +48,6 @@ module ActiveRecord end private - def cache @cache[Process.pid] end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 6782833c5a..c8cefa9906 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -109,8 +109,8 @@ module ActiveRecord # a role. If you would like to use a different role you can pass a hash to database: # # ActiveRecord::Base.connected_to(database: { readonly_slow: :animals_slow_replica }) do - # Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+ - # using the readonly_slow role. + # # runs a long query while connected to the +animals_slow_replica+ using the readonly_slow role. + # Dog.run_a_long_query # end # # When using the database key a new connection will be established every time. @@ -173,7 +173,7 @@ module ActiveRecord raise "Anonymous class is not allowed." unless name config_or_env ||= DEFAULT_ENV.call.to_sym - pool_name = self == Base ? "primary" : name + pool_name = primary_class? ? "primary" : name self.connection_specification_name = pool_name resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations) @@ -204,11 +204,15 @@ module ActiveRecord # Return the specification name from the current class or its parent. def connection_specification_name if !defined?(@connection_specification_name) || @connection_specification_name.nil? - return self == Base ? "primary" : superclass.connection_specification_name + return primary_class? ? "primary" : superclass.connection_specification_name end @connection_specification_name end + def primary_class? # :nodoc: + self == Base || defined?(ApplicationRecord) && self == ApplicationRecord + end + # Returns the configuration of the associated connection as a hash: # # ActiveRecord::Base.connection_config @@ -252,7 +256,6 @@ module ActiveRecord :clear_all_connections!, :flush_idle_connections!, to: :connection_handler private - def swap_connection_handler(handler, &blk) # :nodoc: old_handler, ActiveRecord::Base.connection_handler = ActiveRecord::Base.connection_handler, handler yield diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 6fed3e5c19..595ef4ee25 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -101,7 +101,6 @@ 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. @@ -175,8 +174,7 @@ module ActiveRecord record = statement.execute([id], connection)&.first unless record - raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", - name, primary_key, id) + raise RecordNotFound.new("Couldn't find #{name} with '#{key}'=#{id}", name, key, id) end record end @@ -270,7 +268,8 @@ module ActiveRecord end def arel_attribute(name, table = arel_table) # :nodoc: - name = attribute_alias(name) if attribute_alias?(name) + name = name.to_s + name = attribute_aliases[name] || name table[name] end @@ -287,7 +286,6 @@ module ActiveRecord end private - def cached_find_by_statement(key, &block) cache = @find_by_statement_cache[connection.prepared_statements] cache.compute_if_absent(key) { StatementCache.create(connection, &block) } @@ -318,7 +316,7 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil) - self.class.define_attribute_methods + @new_record = true @attributes = self.class._default_attributes.deep_dup init_internals @@ -355,12 +353,10 @@ module ActiveRecord # +attributes+ should be an attributes object, and unlike the # `initialize` method, no assignment calls are made per attribute. def init_with_attributes(attributes, new_record = false) # :nodoc: - init_internals - @new_record = new_record @attributes = attributes - self.class.define_attribute_methods + init_internals yield self if block_given? @@ -399,13 +395,13 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: @attributes = @attributes.deep_dup - @attributes.reset(self.class.primary_key) + @attributes.reset(@primary_key) _run_initialize_callbacks @new_record = true @destroyed = false - @_start_transaction_state = {} + @_start_transaction_state = nil @transaction_state = nil super @@ -466,7 +462,7 @@ module ActiveRecord # Returns +true+ if the attributes hash has been frozen. def frozen? - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @attributes.frozen? end @@ -557,7 +553,6 @@ module ActiveRecord end private - # +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of # the array, and then rescues from the possible +NoMethodError+. If those elements are # +ActiveRecord::Base+'s, then this triggers the various +method_missing+'s that we have, @@ -571,22 +566,18 @@ module ActiveRecord end def init_internals + @primary_key = self.class.primary_key @readonly = false @destroyed = false @marked_for_destruction = false @destroyed_by_association = nil - @new_record = true - @_start_transaction_state = {} + @_start_transaction_state = nil @transaction_state = nil - end - def initialize_internals_callback + self.class.define_attribute_methods end - def thaw - if @attributes.frozen? - @attributes = @attributes.dup - end + def initialize_internals_callback end def custom_inspect_method_defined? diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index 44b5cfc738..8baa0f5af6 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -104,18 +104,30 @@ module ActiveRecord 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) - end.flatten.compact + db_configs = configs.flat_map do |env_name, config| + if config.is_a?(Hash) && config.all? { |_, v| v.is_a?(Hash) } + walk_configs(env_name.to_s, config) + else + build_db_config_from_raw_config(env_name.to_s, "primary", config) + end + end - if url = ENV["DATABASE_URL"] - build_url_config(url, build_db_config) - else - build_db_config + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s + + unless db_configs.find(&:for_current_env?) + db_configs << environment_url_config(current_env, "primary", {}) + end + + merge_db_environment_variables(current_env, db_configs.compact) + end + + def walk_configs(env_name, config) + config.map do |spec_name, sub_config| + build_db_config_from_raw_config(env_name, spec_name.to_s, sub_config) end end - def walk_configs(env_name, spec_name, config) + def build_db_config_from_raw_config(env_name, spec_name, config) case config when String build_db_config_from_string(env_name, spec_name, config) @@ -141,31 +153,27 @@ module ActiveRecord 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) else - config.each_pair.map do |sub_spec_name, sub_config| - walk_configs(env_name, sub_spec_name, sub_config) - end + ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) end end - def build_url_config(url, configs) - env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s + def merge_db_environment_variables(current_env, configs) + configs.map do |config| + next config if config.url_config? || config.env_name != current_env - if original_config = configs.find(&:for_current_env?) - if original_config.url_config? - configs - else - configs.map do |config| - ActiveRecord::DatabaseConfigurations::UrlConfig.new(config.env_name, config.spec_name, url, config.config) - end - end - else - configs + [ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, "primary", url)] + url_config = environment_url_config(current_env, config.spec_name, config.config) + url_config || config end end + def environment_url_config(env, spec_name, config) + url = ENV["DATABASE_URL"] + return unless url + + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, spec_name, url, config) + end + def method_missing(method, *args, &blk) case method when :each, :first diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb index e2d30ae416..e6b4acc647 100644 --- a/activerecord/lib/active_record/database_configurations/url_config.rb +++ b/activerecord/lib/active_record/database_configurations/url_config.rb @@ -56,7 +56,6 @@ module ActiveRecord end private - def build_url_hash(url) if url.nil? || /^jdbc:/.match?(url) { "url" => url } diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 3bb8c6f4e3..7d9e221faa 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -49,11 +49,11 @@ module ActiveRecord attr_reader :model, :name, :attribute_names - def initialize(model, name) + def initialize(model, method_name) @model = model - @name = name.to_s + @name = method_name.to_s @attribute_names = @name.match(self.class.pattern)[1].split("_and_") - @attribute_names.map! { |n| @model.attribute_aliases[n] || n } + @attribute_names.map! { |name| @model.attribute_aliases[name] || name } end def valid? @@ -69,7 +69,6 @@ module ActiveRecord end private - def body "#{finder}(#{attributes_hash})" end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 8077630aeb..fc49f752aa 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -200,6 +200,8 @@ module ActiveRecord # scope :active, -> { where(status: 0) } # scope :not_active, -> { where.not(status: 0) } if enum_scopes != false + klass.send(:detect_negative_condition!, value_method_name) + klass.send(:detect_enum_conflict!, name, value_method_name, true) klass.scope value_method_name, -> { where(attr => value) } @@ -261,5 +263,12 @@ module ActiveRecord source: source } end + + def detect_negative_condition!(method_name) + if method_name.start_with?("not_") && logger + logger.warn "An enum element in #{self.name} uses the prefix 'not_'." \ + " This will cause a conflict with auto generated negative scopes." + end + end end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 60cf9818c1..20cc987d6e 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -38,6 +38,10 @@ module ActiveRecord class AdapterNotSpecified < ActiveRecordError end + # Raised when a model makes a query but it has not specified an associated table. + class TableNotSpecified < ActiveRecordError + end + # Raised when Active Record cannot find database adapter specified in # +config/database.yml+ or programmatically. class AdapterNotFound < ActiveRecordError @@ -349,16 +353,24 @@ module ActiveRecord class IrreversibleOrderError < ActiveRecordError end + # Superclass for errors that have been aborted (either by client or server). + class QueryAborted < StatementInvalid + end + # LockWaitTimeout will be raised when lock wait timeout exceeded. class LockWaitTimeout < StatementInvalid end # StatementTimeout will be raised when statement timeout exceeded. - class StatementTimeout < StatementInvalid + class StatementTimeout < QueryAborted end # QueryCanceled will be raised when canceling statement due to user request. - class QueryCanceled < StatementInvalid + class QueryCanceled < QueryAborted + end + + # AdapterTimeout will be raised when database clients times out while waiting from the server. + class AdapterTimeout < QueryAborted end # UnknownAttributeReference is raised when an unknown and potentially unsafe diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 919e96cd7a..5dca75c539 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -36,7 +36,6 @@ module ActiveRecord end private - def render_bind(attr) value = if attr.type.binary? && attr.value "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" diff --git a/activerecord/lib/active_record/fixture_set/table_row.rb b/activerecord/lib/active_record/fixture_set/table_row.rb index cb4726f1ee..f65329f91d 100644 --- a/activerecord/lib/active_record/fixture_set/table_row.rb +++ b/activerecord/lib/active_record/fixture_set/table_row.rb @@ -48,7 +48,6 @@ module ActiveRecord end private - def model_metadata @table_rows.model_metadata end diff --git a/activerecord/lib/active_record/fixture_set/table_rows.rb b/activerecord/lib/active_record/fixture_set/table_rows.rb index 23814b6cb5..df1cd63963 100644 --- a/activerecord/lib/active_record/fixture_set/table_rows.rb +++ b/activerecord/lib/active_record/fixture_set/table_rows.rb @@ -29,7 +29,6 @@ module ActiveRecord end private - def build_table_rows_from(table_name, fixtures, config) now = config.default_timezone == :utc ? Time.now.utc : Time.now diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 327121a2a2..046ed0e95c 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -464,7 +464,6 @@ module ActiveRecord end private - def insert_class(class_names, name, klass) # We only want to deal with AR objects. if klass && klass < ActiveRecord::Base @@ -570,7 +569,6 @@ module ActiveRecord end private - def read_and_insert(fixtures_directory, fixture_files, class_names, connection) # :nodoc: fixtures_map = {} fixture_sets = fixture_files.map do |fixture_set_name| @@ -661,7 +659,6 @@ module ActiveRecord end private - def model_class=(class_name) if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index f77bc2e3c1..7f92174f87 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -8,9 +8,9 @@ module ActiveRecord module VERSION MAJOR = 6 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta3" + PRE = "alpha" 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 9570bc6f86..5ca48fa18c 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -176,7 +176,6 @@ module ActiveRecord end protected - # Returns the class type of the record using the current module as a prefix. So descendants of # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. def compute_type(type_name) @@ -208,7 +207,6 @@ module ActiveRecord end private - # Called by +instantiate+ to decide which class to use for a new # record instance. For single-table inheritance, we check the record # for a +type+ column and return the corresponding class. @@ -272,7 +270,6 @@ module ActiveRecord end private - def initialize_internals_callback super ensure_proper_type diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb index 959e5bd4d7..f6577dcbc4 100644 --- a/activerecord/lib/active_record/insert_all.rb +++ b/activerecord/lib/active_record/insert_all.rb @@ -21,9 +21,9 @@ module ActiveRecord end def execute - message = "#{model} " - message += "Bulk " if inserts.many? - message += (on_duplicate == :update ? "Upsert" : "Insert") + message = +"#{model} " + message << "Bulk " if inserts.many? + message << (on_duplicate == :update ? "Upsert" : "Insert") connection.exec_query to_sql, message end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index b769541e95..4a97061731 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -22,6 +22,14 @@ module ActiveRecord # # This is +true+, by default on Rails 5.2 and above. class_attribute :cache_versioning, instance_writer: false, default: false + + ## + # :singleton-method: + # Indicates whether to use a stable #cache_key method that is accompanied + # by a changing version in the #cache_version method on collections. + # + # This is +false+, by default until Rails 6.1. + class_attribute :collection_cache_versioning, instance_writer: false, default: false end # Returns a +String+, which Action Pack uses for constructing a URL to this @@ -85,7 +93,7 @@ module ActiveRecord # cache_version, but this method can be overwritten to return something else. # # Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to - # +false+ (which it is by default until Rails 6.0). + # +false+. def cache_version return unless cache_versioning @@ -154,7 +162,7 @@ module ActiveRecord end def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc: - collection.compute_cache_key(timestamp_column) + collection.send(:compute_cache_key, timestamp_column) end end diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index e6166581f1..8f3c6d0ee3 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -28,10 +28,6 @@ module ActiveRecord where(key: key).pluck(:value).first end - def table_exists? - connection.table_exists?(table_name) - end - # Creates an internal metadata table with columns +key+ and +value+ def create_table unless table_exists? diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index b7eecda59e..c2a083bf3b 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -87,7 +87,7 @@ module ActiveRecord affected_rows = self.class._update_record( attributes_with_values(attribute_names), - self.class.primary_key => id_in_database, + @primary_key => id_in_database, locking_column => previous_lock_value ) @@ -110,7 +110,7 @@ module ActiveRecord locking_column = self.class.locking_column affected_rows = self.class._delete_record( - self.class.primary_key => id_in_database, + @primary_key => id_in_database, locking_column => read_attribute_before_type_cast(locking_column) ) @@ -156,7 +156,6 @@ module ActiveRecord end private - # We need to apply this decorator here, rather than on module inclusion. The closure # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the # sub class being decorated. As such, changes to `lock_optimistically`, or diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 6b84431343..6248c2f578 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -110,7 +110,7 @@ module ActiveRecord end def extract_query_source_location(locations) - backtrace_cleaner.clean(locations).first + backtrace_cleaner.clean(locations.lazy).first end end end diff --git a/activerecord/lib/active_record/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb index b5b5df074c..7374107048 100644 --- a/activerecord/lib/active_record/middleware/database_selector.rb +++ b/activerecord/lib/active_record/middleware/database_selector.rb @@ -35,10 +35,10 @@ module ActiveRecord # 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 = {}) + def initialize(app, resolver_klass = nil, context_klass = nil, options = {}) @app = app - @resolver_klass = resolver_klass - @context_klass = context_klass + @resolver_klass = resolver_klass || Resolver + @context_klass = context_klass || Resolver::Session @options = options end @@ -55,7 +55,6 @@ module ActiveRecord end private - def select_database(request, &blk) context = context_klass.call(request) resolver = resolver_klass.call(context, options) diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver.rb b/activerecord/lib/active_record/middleware/database_selector/resolver.rb index 80b8cd7cae..3eb1039c50 100644 --- a/activerecord/lib/active_record/middleware/database_selector/resolver.rb +++ b/activerecord/lib/active_record/middleware/database_selector/resolver.rb @@ -44,10 +44,9 @@ module ActiveRecord end private - def read_from_primary(&blk) - ActiveRecord::Base.connection.while_preventing_writes do - ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do + ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do + ActiveRecord::Base.connection_handler.while_preventing_writes do instrumenter.instrument("database_selector.active_record.read_from_primary") do yield end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index ed0c6d48b8..7edfec9903 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -4,6 +4,7 @@ require "benchmark" require "set" require "zlib" require "active_support/core_ext/module/attribute_accessors" +require "active_support/actionable_error" module ActiveRecord class MigrationError < ActiveRecordError #:nodoc: @@ -128,6 +129,12 @@ module ActiveRecord end class PendingMigrationError < MigrationError #:nodoc: + include ActiveSupport::ActionableError + + action "Run pending migrations" do + ActiveRecord::Tasks::DatabaseTasks.migrate + end + def initialize(message = nil) if !message && defined?(Rails.env) super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate RAILS_ENV=#{::Rails.env}") @@ -487,9 +494,9 @@ module ActiveRecord # This migration will create the horses table for you on the way up, and # automatically figure out how to drop the table on the way down. # - # Some commands like +remove_column+ cannot be reversed. If you care to - # define how to move up and down in these cases, you should define the +up+ - # and +down+ methods as before. + # Some commands cannot be reversed. If you care to define how to move up + # and down in these cases, you should define the +up+ and +down+ methods + # as before. # # If a command cannot be reversed, an # <tt>ActiveRecord::IrreversibleMigration</tt> exception will be raised when @@ -561,7 +568,6 @@ module ActiveRecord end private - def connection ActiveRecord::Base.connection end @@ -878,13 +884,14 @@ module ActiveRecord def copy(destination, sources, options = {}) copied = [] + schema_migration = options[:schema_migration] || ActiveRecord::SchemaMigration FileUtils.mkdir_p(destination) unless File.exist?(destination) - destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations + destination_migrations = ActiveRecord::MigrationContext.new(destination, schema_migration).migrations last = destination_migrations.last sources.each do |scope, path| - source_migrations = ActiveRecord::MigrationContext.new(path).migrations + source_migrations = ActiveRecord::MigrationContext.new(path, schema_migration).migrations source_migrations.each do |migration| source = File.binread(migration.filename) @@ -985,7 +992,6 @@ module ActiveRecord delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration private - def migration @migration ||= load_migration end @@ -1007,10 +1013,11 @@ module ActiveRecord end class MigrationContext #:nodoc: - attr_reader :migrations_paths + attr_reader :migrations_paths, :schema_migration - def initialize(migrations_paths) + def initialize(migrations_paths, schema_migration) @migrations_paths = migrations_paths + @schema_migration = schema_migration end def migrate(target_version = nil, &block) @@ -1041,7 +1048,7 @@ module ActiveRecord migrations end - Migrator.new(:up, selected_migrations, target_version).migrate + Migrator.new(:up, selected_migrations, schema_migration, target_version).migrate end def down(target_version = nil) @@ -1051,20 +1058,20 @@ module ActiveRecord migrations end - Migrator.new(:down, selected_migrations, target_version).migrate + Migrator.new(:down, selected_migrations, schema_migration, target_version).migrate end def run(direction, target_version) - Migrator.new(direction, migrations, target_version).run + Migrator.new(direction, migrations, schema_migration, target_version).run end def open - Migrator.new(:up, migrations, nil) + Migrator.new(:up, migrations, schema_migration) end def get_all_versions - if SchemaMigration.table_exists? - SchemaMigration.all_versions.map(&:to_i) + if schema_migration.table_exists? + schema_migration.all_versions.map(&:to_i) else [] end @@ -1101,12 +1108,12 @@ module ActiveRecord end def migrations_status - db_list = ActiveRecord::SchemaMigration.normalized_versions + db_list = schema_migration.normalized_versions file_list = migration_files.map do |file| version, name, scope = parse_migration_filename(file) raise IllegalMigrationNameError.new(file) unless version - version = ActiveRecord::SchemaMigration.normalize_migration_number(version) + version = schema_migration.normalize_migration_number(version) status = db_list.delete(version) ? "up" : "down" [status, version, (name + scope).humanize] end.compact @@ -1146,7 +1153,7 @@ module ActiveRecord end def move(direction, steps) - migrator = Migrator.new(direction, migrations) + migrator = Migrator.new(direction, migrations, schema_migration) if current_version != 0 && !migrator.current_migration raise UnknownMigrationVersionError.new(current_version) @@ -1165,27 +1172,28 @@ module ActiveRecord end end - class Migrator #:nodoc: + class Migrator # :nodoc: class << self attr_accessor :migrations_paths # For cases where a table doesn't exist like loading from schema cache def current_version - MigrationContext.new(migrations_paths).current_version + MigrationContext.new(migrations_paths, SchemaMigration).current_version end end self.migrations_paths = ["db/migrate"] - def initialize(direction, migrations, target_version = nil) + def initialize(direction, migrations, schema_migration, target_version = nil) @direction = direction @target_version = target_version @migrated_versions = nil @migrations = migrations + @schema_migration = schema_migration validate(@migrations) - ActiveRecord::SchemaMigration.create_table + @schema_migration.create_table ActiveRecord::InternalMetadata.create_table end @@ -1239,11 +1247,10 @@ module ActiveRecord end def load_migrated - @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions) + @migrated_versions = Set.new(@schema_migration.all_versions.map(&:to_i)) end private - # Used for running a specific migration. def run_without_lock migration = migrations.detect { |m| m.version == @target_version } @@ -1323,10 +1330,10 @@ module ActiveRecord def record_version_state_after_migrating(version) if down? migrated.delete(version) - ActiveRecord::SchemaMigration.delete_by(version: version.to_s) + @schema_migration.delete_by(version: version.to_s) else migrated << version - ActiveRecord::SchemaMigration.create!(version: version.to_s) + @schema_migration.create!(version: version.to_s) end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index efed4b0e26..67172ef395 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -118,7 +118,6 @@ module ActiveRecord end private - module StraightReversions # :nodoc: private { diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index ff91218696..ef78a9161e 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -13,7 +13,10 @@ module ActiveRecord const_get(name) end - V6_0 = Current + V6_1 = Current + + class V6_0 < V6_1 + end class V5_2 < V6_0 module TableDefinition diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb index 9abb289bb0..45169617c1 100644 --- a/activerecord/lib/active_record/migration/join_table.rb +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -4,7 +4,6 @@ module ActiveRecord class Migration module JoinTable #:nodoc: private - def find_join_table_name(table_1, table_2, options = {}) options.delete(:table_name) || join_table_name(table_1, table_2) end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 55fc58e339..18f19af6be 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -456,13 +456,11 @@ module ActiveRecord end protected - def initialize_load_schema_monitor @load_schema_monitor = Monitor.new end private - def inherited(child_class) super child_class.initialize_load_schema_monitor @@ -484,6 +482,9 @@ module ActiveRecord end def load_schema! + unless table_name + raise ActiveRecord::TableNotSpecified, "#{self} has no table configured. Set one with #{self}.table_name=" + end @columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns) @columns_hash.each do |name, column| define_attribute( diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 8b9098df6c..ab107742ed 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -2,7 +2,6 @@ require "active_support/core_ext/hash/except" require "active_support/core_ext/module/redefine_method" -require "active_support/core_ext/object/try" require "active_support/core_ext/hash/indifferent_access" module ActiveRecord @@ -354,7 +353,6 @@ module ActiveRecord end private - # Generates a writer method for this association. Serves as a point for # accessing the objects in the association. For example, this method # could generate the following: @@ -386,7 +384,6 @@ module ActiveRecord end private - # Attribute hash keys that should not be assigned as normal attributes. # These hash keys are nested attributes implementation details. UNASSIGNABLE_KEYS = %w( id _destroy ) diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index cf0de0fdeb..bee5b5f24a 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -60,7 +60,6 @@ module ActiveRecord end private - def exec_queries @records = [].freeze end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 8bade8cd28..323b01ab2d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -353,6 +353,7 @@ module ActiveRecord end def _insert_record(values) # :nodoc: + primary_key = self.primary_key primary_key_value = nil if primary_key && Hash === values @@ -423,20 +424,20 @@ module ActiveRecord # Returns true if this object hasn't been saved yet -- that is, a record # for the object doesn't exist in the database yet; otherwise, returns false. def new_record? - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @new_record end # Returns true if this object has been destroyed, otherwise returns false. def destroyed? - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? @destroyed end # Returns true if the record is persisted, i.e. it's not a new record and it was # not destroyed, otherwise returns false. def persisted? - sync_with_transaction_state + sync_with_transaction_state if @transaction_state&.finalized? !(@new_record || @destroyed) end @@ -663,8 +664,13 @@ module ActiveRecord raise ActiveRecordError, "cannot update a new record" if new_record? raise ActiveRecordError, "cannot update a destroyed record" if destroyed? + attributes = attributes.transform_keys do |key| + name = key.to_s + self.class.attribute_aliases[name] || name + end + attributes.each_key do |key| - verify_readonly_attribute(key.to_s) + verify_readonly_attribute(key) end id_in_database = self.id_in_database @@ -674,7 +680,7 @@ module ActiveRecord affected_rows = self.class._update_record( attributes, - self.class.primary_key => id_in_database + @primary_key => id_in_database ) affected_rows == 1 @@ -843,16 +849,11 @@ module ActiveRecord # ball.touch(:updated_at) # => raises ActiveRecordError # def touch(*names, time: nil) - unless persisted? - raise ActiveRecordError, <<-MSG.squish - cannot touch on a new or destroyed record object. Consider using - persisted?, new_record?, or destroyed? before touching - MSG - end + _raise_record_not_touched_error unless persisted? attribute_names = timestamp_attributes_for_update_in_model attribute_names |= names.map!(&:to_s).map! { |name| - self.class.attribute_alias?(name) ? self.class.attribute_alias(name) : name + self.class.attribute_aliases[name] || name } unless attribute_names.empty? @@ -864,7 +865,6 @@ module ActiveRecord end private - # A hook to be overridden by association modules. def destroy_associations end @@ -874,7 +874,7 @@ module ActiveRecord end def _delete_row - self.class._delete_record(self.class.primary_key => id_in_database) + self.class._delete_record(@primary_key => id_in_database) end def _touch_row(attribute_names, time) @@ -890,7 +890,7 @@ module ActiveRecord def _update_row(attribute_names, attempted_action = "update") self.class._update_record( attributes_with_values(attribute_names), - self.class.primary_key => id_in_database + @primary_key => id_in_database ) end @@ -928,7 +928,7 @@ module ActiveRecord attributes_with_values(attribute_names) ) - self.id ||= new_id if self.class.primary_key + self.id ||= new_id if @primary_key @new_record = false @@ -938,7 +938,7 @@ module ActiveRecord end def verify_readonly_attribute(name) - raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) + raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attribute?(name) end def _raise_record_not_destroyed @@ -948,14 +948,21 @@ module ActiveRecord @_association_destroy_exception = nil end + def _raise_readonly_record_error + raise ReadOnlyRecord, "#{self.class} is marked as readonly" + end + + def _raise_record_not_touched_error + raise ActiveRecordError, <<~MSG.squish + Cannot touch on a new or destroyed record object. Consider using + persisted?, new_record?, or destroyed? before touching. + MSG + end + # The name of the method used to touch a +belongs_to+ association when the # +:touch+ option is used. def belongs_to_touch_method :touch end - - def _raise_readonly_record_error - raise ReadOnlyRecord, "#{self.class} is marked as readonly" - end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index a1d7c893bf..d5375390c7 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -134,7 +134,6 @@ end_error cache = YAML.load(File.read(filename)) if cache.version == current_version - connection.schema_cache = cache connection_pool.schema_cache = cache.dup else warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{current_version}, but the one in the cache is #{cache.version}." diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 447def8d77..4d9acc911b 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -2,6 +2,8 @@ require "active_record" +databases = ActiveRecord::Tasks::DatabaseTasks.setup_initial_database_yaml + db_namespace = namespace :db do desc "Set the environment value for the database" task "environment:set" => :load_config do @@ -23,7 +25,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.create_all end - ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name| desc "Create #{spec_name} database for current environment" task spec_name => :load_config do db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) @@ -42,7 +44,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.drop_all end - ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name| desc "Drop #{spec_name} database for current environment" task spec_name => [:load_config, :check_protected_environments] do db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) @@ -78,7 +80,7 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task migrate: :load_config do - ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config| ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::Tasks::DatabaseTasks.migrate end @@ -101,7 +103,7 @@ db_namespace = namespace :db do end namespace :migrate do - ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name| desc "Migrate #{spec_name} database for current environment" task spec_name => :load_config do db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) @@ -110,7 +112,7 @@ db_namespace = namespace :db do end end - # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' + desc "Rolls back the database one migration and re-migrates up (options: STEP=x, VERSION=x)." task redo: :load_config do raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? @@ -126,8 +128,10 @@ db_namespace = namespace :db do # desc 'Resets your database using your migrations for the current environment' task reset: ["db:drop", "db:create", "db:migrate"] - # desc 'Runs the "up" for a given migration VERSION.' + desc 'Runs the "up" for a given migration VERSION.' task up: :load_config do + ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:up") + raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? ActiveRecord::Tasks::DatabaseTasks.check_target_version @@ -139,8 +143,29 @@ db_namespace = namespace :db do db_namespace["_dump"].invoke end - # desc 'Runs the "down" for a given migration VERSION.' + namespace :up do + ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name| + task spec_name => :load_config do + raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? + + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) + + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.check_target_version + ActiveRecord::Base.connection.migration_context.run( + :up, + ActiveRecord::Tasks::DatabaseTasks.target_version + ) + + db_namespace["_dump"].invoke + end + end + end + + desc 'Runs the "down" for a given migration VERSION.' task down: :load_config do + ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:down") + raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty? ActiveRecord::Tasks::DatabaseTasks.check_target_version @@ -152,16 +177,35 @@ db_namespace = namespace :db do db_namespace["_dump"].invoke end + namespace :down do + ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name| + task spec_name => :load_config do + raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? + + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) + + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.check_target_version + ActiveRecord::Base.connection.migration_context.run( + :down, + ActiveRecord::Tasks::DatabaseTasks.target_version + ) + + db_namespace["_dump"].invoke + end + end + end + desc "Display status of migrations" task status: :load_config do - ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config| ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::Tasks::DatabaseTasks.migrate_status end end namespace :status do - ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name| desc "Display status of migrations for #{spec_name} database" task spec_name => :load_config do db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) @@ -186,7 +230,7 @@ db_namespace = namespace :db do db_namespace["_dump"].invoke end - # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' + desc "Drops and recreates the database from db/schema.rb for the current environment and loads the seeds." task reset: [ "db:drop", "db:setup" ] # desc "Retrieves the charset for the current environment's database" @@ -208,7 +252,11 @@ db_namespace = namespace :db do # desc "Raises an error if there are pending migrations" task abort_if_pending_migrations: :load_config do - pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations + pending_migrations = ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).flat_map do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + + ActiveRecord::Base.connection.migration_context.open.pending_migrations + end if pending_migrations.any? puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" @@ -219,17 +267,57 @@ db_namespace = namespace :db do end end + namespace :abort_if_pending_migrations do + ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name| + # desc "Raises an error if there are pending migrations for #{spec_name} database" + task spec_name => :load_config do + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) + ActiveRecord::Base.establish_connection(db_config.config) + + pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations + + if pending_migrations.any? + puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" + pending_migrations.each do |pending_migration| + puts " %4d %s" % [pending_migration.version, pending_migration.name] + end + abort %{Run `rails db:migrate:#{spec_name}` to update your database then try again.} + end + end + end + end + desc "Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)" task setup: ["db:schema:load_if_ruby", "db:structure:load_if_sql", :seed] desc "Runs setup if database does not exist, or runs migrations if it does" task prepare: :load_config do - ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + seed = false + + ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config| ActiveRecord::Base.establish_connection(db_config.config) - db_namespace["migrate"].invoke + + # Skipped when no database + ActiveRecord::Tasks::DatabaseTasks.migrate + if ActiveRecord::Base.dump_schema_after_migration + ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, ActiveRecord::Base.schema_format, db_config.spec_name) + end + rescue ActiveRecord::NoDatabaseError - db_namespace["setup"].invoke + ActiveRecord::Tasks::DatabaseTasks.create_current(db_config.env_name, db_config.spec_name) + ActiveRecord::Tasks::DatabaseTasks.load_schema( + db_config.config, + ActiveRecord::Base.schema_format, + nil, + db_config.env_name, + db_config.spec_name + ) + + seed = true end + + ActiveRecord::Base.establish_connection + ActiveRecord::Tasks::DatabaseTasks.load_seed if seed end desc "Loads the seed data from db/seeds.rb" @@ -294,13 +382,9 @@ db_namespace = namespace :db do namespace :schema do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" task dump: :load_config do - require "active_record/schema_dumper" - ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| - filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby) - File.open(filename, "w:utf-8") do |file| - ActiveRecord::Base.establish_connection(db_config.config) - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) - end + ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, :ruby, db_config.spec_name) end db_namespace["schema:dump"].reenable @@ -318,7 +402,7 @@ db_namespace = namespace :db do namespace :cache do desc "Creates a db/schema_cache.yml file." task dump: :load_config do - ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.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( @@ -330,7 +414,7 @@ db_namespace = namespace :db do desc "Clears a db/schema_cache.yml file." task clear: :load_config do - ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config| filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name) rm_f filename, verbose: false end @@ -341,16 +425,9 @@ db_namespace = namespace :db do namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" task dump: :load_config do - ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config| ActiveRecord::Base.establish_connection(db_config.config) - filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql) - ActiveRecord::Tasks::DatabaseTasks.structure_dump(db_config.config, filename) - if ActiveRecord::SchemaMigration.table_exists? - File.open(filename, "a") do |f| - f.puts ActiveRecord::Base.connection.dump_schema_information - f.print "\n" - end - end + ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, :sql, db_config.spec_name) end db_namespace["structure:dump"].reenable diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 7bc26993d5..c851ed52c3 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -19,6 +19,10 @@ module ActiveRecord def readonly_attributes _attr_readonly end + + def readonly_attribute?(name) # :nodoc: + _attr_readonly.include?(name) + end end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 1312bf6f91..cbfa60d4d9 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -477,7 +477,7 @@ module ActiveRecord def check_preloadable! return unless scope - if scope.arity > 0 + unless scope.arity == 0 raise ArgumentError, <<-MSG.squish The association scope '#{name}' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is @@ -590,7 +590,6 @@ module ActiveRecord end private - def calculate_constructable(macro, options) true end @@ -704,7 +703,6 @@ module ActiveRecord end private - def calculate_constructable(macro, options) !options[:through] end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index cd62b0b881..ea8f44752b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -291,36 +291,58 @@ module ActiveRecord limit_value ? records.many? : size > 1 end - # Returns a cache key that can be used to identify the records fetched by - # this query. The cache key is built with a fingerprint of the sql query, - # the number of records matched by the query and a timestamp of the last - # updated record. When a new record comes to match the query, or any of - # the existing records is updated or deleted, the cache key changes. + # Returns a stable cache key that can be used to identify this query. + # The cache key is built with a fingerprint of the SQL query. # - # Product.where("name like ?", "%Cosmic Encounter%").cache_key - # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000" + # Product.where("name like ?", "%Cosmic Encounter%").cache_key + # # => "products/query-1850ab3d302391b85b8693e941286659" # - # If the collection is loaded, the method will iterate through the records - # to generate the timestamp, otherwise it will trigger one SQL query like: + # If ActiveRecord::Base.collection_cache_versioning is turned off, as it was + # in Rails 6.0 and earlier, the cache key will also include a version. # - # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%') + # ActiveRecord::Base.collection_cache_versioning = false + # Product.where("name like ?", "%Cosmic Encounter%").cache_key + # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000" # # You can also pass a custom timestamp column to fetch the timestamp of the # last updated record. # # Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at) - # - # You can customize the strategy to generate the key on a per model basis - # overriding ActiveRecord::Base#collection_cache_key. def cache_key(timestamp_column = :updated_at) @cache_keys ||= {} - @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column) + @cache_keys[timestamp_column] ||= klass.collection_cache_key(self, timestamp_column) end def compute_cache_key(timestamp_column = :updated_at) # :nodoc: query_signature = ActiveSupport::Digest.hexdigest(to_sql) key = "#{klass.model_name.cache_key}/query-#{query_signature}" + if cache_version(timestamp_column) + key + else + "#{key}-#{compute_cache_version(timestamp_column)}" + end + end + private :compute_cache_key + + # Returns a cache version that can be used together with the cache key to form + # a recyclable caching scheme. The cache version is built with the number of records + # matching the query, and the timestamp of the last updated record. When a new record + # comes to match the query, or any of the existing records is updated or deleted, + # the cache version changes. + # + # If the collection is loaded, the method will iterate through the records + # to generate the timestamp, otherwise it will trigger one SQL query like: + # + # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%') + def cache_version(timestamp_column = :updated_at) + if collection_cache_versioning + @cache_versions ||= {} + @cache_versions[timestamp_column] ||= compute_cache_version(timestamp_column) + end + end + + def compute_cache_version(timestamp_column) # :nodoc: if loaded? || distinct_value size = records.size if size > 0 @@ -356,11 +378,12 @@ module ActiveRecord end if timestamp - "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}" + "#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}" else - "#{key}-#{size}" + "#{size}" end end + private :compute_cache_version # Scope all queries to the current scope. # @@ -445,7 +468,19 @@ module ActiveRecord end end - def update_counters(counters) # :nodoc: + # Updates the counters of the records in the current relation. + # + # === Parameters + # + # * +counter+ - A Hash containing the names of the fields to update as keys and the amount to update as values. + # * <tt>:touch</tt> option - Touch the timestamp columns when updating. + # * If attributes names are passed, they are updated along with update_at/on attributes. + # + # === Examples + # + # # For Posts by a given author increment the comment_count by 1. + # Post.where(author_id: author.id).update_counters(comment_count: 1) + def update_counters(counters) touch = counters.delete(:touch) updates = {} @@ -530,8 +565,8 @@ module ActiveRecord # # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct def delete_all invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method| - value = get_value(method) - SINGLE_VALUE_METHODS.include?(method) ? value : value.any? + value = @values[method] + method == :distinct ? value : value&.any? end if invalid_methods.any? raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}") diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 9c579843b1..30b8edd0bd 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -258,7 +258,6 @@ module ActiveRecord end private - def apply_limits(relation, start, finish) relation = apply_start_limit(relation, start) if start relation = apply_finish_limit(relation, finish) if finish diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 801e312658..0a14a33c1d 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -260,10 +260,8 @@ module ActiveRecord def aggregate_column(column_name) return column_name if Arel::Expressions === column_name - if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name) - @klass.arel_attribute(column_name) - else - Arel.sql(column_name == :all ? "*" : column_name.to_s) + arel_column(column_name.to_s) do |name| + Arel.sql(column_name == :all ? "*" : name) end end @@ -342,7 +340,7 @@ module ActiveRecord } relation = except(:group).distinct!(false) - relation.group_values = group_aliases + relation.group_values = group_fields relation.select_values = select_values calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil) } diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 7a53a9d1c7..2f61c05eca 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -45,7 +45,10 @@ module ActiveRecord private def generated_relation_methods - @generated_relation_methods ||= GeneratedRelationMethods.new + @generated_relation_methods ||= GeneratedRelationMethods.new.tap do |mod| + const_set(:GeneratedRelationMethods, mod) + private_constant :GeneratedRelationMethods + end end end @@ -96,7 +99,6 @@ module ActiveRecord end private - def method_missing(method, *args, &block) if @klass.respond_to?(method) @klass.generate_relation_method(method) @@ -113,7 +115,6 @@ module ActiveRecord end private - def relation_class_for(klass) klass.relation_delegate_class(self) end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 9450e4d3c5..1dbf4808fd 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -346,7 +346,6 @@ module ActiveRecord end private - def offset_index offset_value || 0 end @@ -355,7 +354,7 @@ module ActiveRecord conditions = sanitize_forbidden_attributes(conditions) if distinct_value && offset_value - relation = limit(1) + relation = except(:order).limit!(1) else relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) end @@ -371,7 +370,9 @@ module ActiveRecord end def apply_join_dependency(eager_loading: group_values.empty?) - join_dependency = construct_join_dependency(eager_load_values + includes_values) + join_dependency = construct_join_dependency( + eager_load_values + includes_values, Arel::Nodes::OuterJoin + ) relation = except(:includes, :eager_load, :preload).joins!(join_dependency) if eager_loading && !using_limitable_reflections?(join_dependency.reflections) diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 6bb77b355c..e1735c0522 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -89,7 +89,6 @@ module ActiveRecord end private - def merge_preloads return if other.preload_values.empty? && other.includes_values.empty? @@ -123,7 +122,9 @@ module ActiveRecord end end - join_dependency = other.construct_join_dependency(associations) + join_dependency = other.construct_join_dependency( + associations, Arel::Nodes::InnerJoin + ) relation.joins!(join_dependency, *others) end end @@ -135,7 +136,9 @@ module ActiveRecord relation.left_outer_joins!(*other.left_outer_joins_values) else associations = other.left_outer_joins_values - join_dependency = other.construct_join_dependency(associations) + join_dependency = other.construct_join_dependency( + associations, Arel::Nodes::OuterJoin + ) relation.joins!(join_dependency) end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 90b5e9a118..6957ba052b 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -41,18 +41,31 @@ module ActiveRecord # # User.where.not(name: %w(Ko1 Nobu)) # # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu') - # - # User.where.not(name: "Jon", role: "admin") - # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' def not(opts, *rest) opts = sanitize_forbidden_attributes(opts) where_clause = @scope.send(:where_clause_factory).build(opts, rest) @scope.references!(PredicateBuilder.references(opts)) if Hash === opts - @scope.where_clause += where_clause.invert + + if not_behaves_as_nor?(opts) + ActiveSupport::Deprecation.warn(<<~MSG.squish) + NOT conditions will no longer behave as NOR in Rails 6.1. + To continue using NOR conditions, NOT each conditions manually + (`#{ opts.keys.map { |key| ".where.not(#{key.inspect} => ...)" }.join }`). + MSG + @scope.where_clause += where_clause.invert(:nor) + else + @scope.where_clause += where_clause.invert + end + @scope end + + private + def not_behaves_as_nor?(opts) + opts.is_a?(Hash) && opts.size > 1 + end end FROZEN_EMPTY_ARRAY = [].freeze @@ -67,11 +80,13 @@ module ActiveRecord end class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{method_name} # def includes_values - get_value(#{name.inspect}) # get_value(:includes) + default = DEFAULT_VALUES[:#{name}] # default = DEFAULT_VALUES[:includes] + @values.fetch(:#{name}, default) # @values.fetch(:includes, default) end # end def #{method_name}=(value) # def includes_values=(value) - set_value(#{name.inspect}, value) # set_value(:includes, value) + assert_mutability! # assert_mutability! + @values[:#{name}] = value # @values[:includes] = value end # end CODE end @@ -123,7 +138,7 @@ module ActiveRecord end def includes!(*args) # :nodoc: - args.reject!(&:blank?) + args.compact_blank! args.flatten! self.includes_values |= args @@ -250,7 +265,7 @@ module ActiveRecord end def _select!(*fields) # :nodoc: - fields.reject!(&:blank?) + fields.compact_blank! fields.flatten! self.select_values += fields self @@ -417,7 +432,8 @@ module ActiveRecord if !VALID_UNSCOPING_VALUES.include?(scope) raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." end - set_value(scope, DEFAULT_VALUES[scope]) + assert_mutability! + @values[scope] = DEFAULT_VALUES[scope] when Hash scope.each do |key, target_value| if key != :where @@ -936,7 +952,7 @@ module ActiveRecord def optimizer_hints!(*args) # :nodoc: args.flatten! - self.optimizer_hints_values += args + self.optimizer_hints_values |= args self end @@ -949,7 +965,7 @@ module ActiveRecord def reverse_order! # :nodoc: orders = order_values.uniq - orders.reject!(&:blank?) + orders.compact_blank! self.order_values = reverse_sql_order(orders) self end @@ -989,9 +1005,9 @@ module ActiveRecord @arel ||= build_arel(aliases) end - def construct_join_dependency(associations) # :nodoc: + def construct_join_dependency(associations, join_type) # :nodoc: ActiveRecord::Associations::JoinDependency.new( - klass, table, associations + klass, table, associations, join_type ) end @@ -1005,17 +1021,6 @@ module ActiveRecord end private - # Returns a relation value with a given name - def get_value(name) - @values.fetch(name, DEFAULT_VALUES[name]) - end - - # Sets the relation value with the given name - def set_value(name, value) - assert_mutability! - @values[name] = value - end - def assert_mutability! raise ImmutableRelation if @loaded raise ImmutableRelation if defined?(@arel) && @arel @@ -1048,7 +1053,7 @@ module ActiveRecord ) arel.skip(Arel::Nodes::BindParam.new(offset_attribute)) end - arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? + arel.group(*arel_columns(group_values.uniq.compact_blank)) unless group_values.empty? build_order(arel) @@ -1097,7 +1102,7 @@ module ActiveRecord def build_joins(manager, joins, aliases) unless left_outer_joins_values.empty? left_joins = valid_association_list(left_outer_joins_values.flatten) - joins << construct_join_dependency(left_joins) + joins.unshift construct_join_dependency(left_joins, Arel::Nodes::OuterJoin) end buckets = joins.group_by do |join| @@ -1123,27 +1128,21 @@ module ActiveRecord association_joins = buckets[:association_join] stashed_joins = buckets[:stashed_join] - join_nodes = buckets[:join_node].uniq - string_joins = buckets[:string_join].map(&:strip).uniq + join_nodes = buckets[:join_node].tap(&:uniq!) + string_joins = buckets[:string_join].compact_blank!.map!(&:strip).tap(&:uniq!) - join_list = join_nodes + convert_join_strings_to_ast(string_joins) - alias_tracker = alias_tracker(join_list, aliases) + string_joins.map! { |join| table.create_string_join(Arel.sql(join)) } - join_dependency = construct_join_dependency(association_joins) + join_sources = manager.join_sources + join_sources.concat(join_nodes) unless join_nodes.empty? - joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker) - joins.each { |join| manager.from(join) } - - manager.join_sources.concat(join_list) - - alias_tracker.aliases - end + unless association_joins.empty? && stashed_joins.empty? + alias_tracker = alias_tracker(join_nodes + string_joins, aliases) + join_dependency = construct_join_dependency(association_joins, join_type) + join_sources.concat(join_dependency.join_constraints(stashed_joins, alias_tracker)) + end - def convert_join_strings_to_ast(joins) - joins - .flatten - .reject(&:blank?) - .map { |join| table.create_string_join(Arel.sql(join)) } + join_sources.concat(string_joins) unless string_joins.empty? end def build_select(arel) @@ -1160,8 +1159,9 @@ module ActiveRecord columns.flat_map do |field| case field when Symbol - field = field.to_s - arel_column(field, &connection.method(:quote_table_name)) + arel_column(field.to_s) do |attr_name| + connection.quote_table_name(attr_name) + end when String arel_column(field, &:itself) when Proc @@ -1173,7 +1173,7 @@ module ActiveRecord end def arel_column(field) - field = klass.attribute_alias(field) if klass.attribute_alias?(field) + field = klass.attribute_aliases[field] || field from = from_clause.name || from_clause.value if klass.columns_hash.key?(field) && (!from || table_name_matches?(from)) @@ -1227,7 +1227,7 @@ module ActiveRecord def build_order(arel) orders = order_values.uniq - orders.reject!(&:blank?) + orders.compact_blank! arel.order(*orders) unless orders.empty? end @@ -1248,6 +1248,7 @@ module ActiveRecord end def preprocess_order_args(order_args) + order_args.reject!(&:blank?) order_args.map! do |arg| klass.sanitize_sql_for_order(arg) end @@ -1255,7 +1256,7 @@ module ActiveRecord @klass.disallow_raw_sql!( order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a }, - permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER + permit: connection.column_name_with_order_matcher ) validate_order_args(order_args) @@ -1268,20 +1269,14 @@ module ActiveRecord order_args.map! do |arg| case arg when Symbol - arg = arg.to_s - arel_column(arg) { - Arel.sql(connection.quote_table_name(arg)) - }.asc + order_column(arg.to_s).asc when Hash arg.map { |field, dir| case field when Arel::Nodes::SqlLiteral field.send(dir.downcase) else - field = field.to_s - arel_column(field) { - Arel.sql(connection.quote_table_name(field)) - }.send(dir.downcase) + order_column(field.to_s).send(dir.downcase) end } else @@ -1290,6 +1285,16 @@ module ActiveRecord end.flatten! end + def order_column(field) + arel_column(field) do |attr_name| + if attr_name == "count" && !group_values.empty? + arel_attribute(attr_name) + else + Arel.sql(connection.quote_table_name(attr_name)) + end + end + end + # Checks to make sure that the arguments are not blank. Note that if some # blank-like object were initially passed into the query method, then this # method will not raise an error. @@ -1316,7 +1321,8 @@ module ActiveRecord def structurally_incompatible_values_for_or(other) values = other.values STRUCTURAL_OR_METHODS.reject do |method| - get_value(method) == values.fetch(method, DEFAULT_VALUES[method]) + default = DEFAULT_VALUES[method] + @values.fetch(method, default) == values.fetch(method, default) end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index efc4b447aa..3f6dd50139 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -67,7 +67,6 @@ module ActiveRecord end private - def relation_with(values) result = Relation.create(klass, values: values) result.extend(*extending_values) if extending_values.any? diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 47728aac30..8fae380b0a 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -70,7 +70,15 @@ module ActiveRecord predicates == other.predicates end - def invert + def invert(as = :nand) + if predicates.size == 1 + inverted_predicates = [ invert_predicate(predicates.first) ] + elsif as == :nor + inverted_predicates = predicates.map { |node| invert_predicate(node) } + else + inverted_predicates = [ Arel::Nodes::Not.new(ast) ] + end + WhereClause.new(inverted_predicates) end @@ -79,7 +87,6 @@ module ActiveRecord end protected - attr_reader :predicates def referenced_columns @@ -115,10 +122,6 @@ module ActiveRecord node.respond_to?(:operator) && node.operator == :== end - def inverted_predicates - predicates.map { |node| invert_predicate(node) } - end - def invert_predicate(node) case node when NilClass diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index da6d10b6ec..3b615f29a3 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -132,7 +132,6 @@ module ActiveRecord end private - def column_type(name, type_overrides = {}) type_overrides.fetch(name) do column_types.fetch(name, Type.default_value) diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 750766714d..b16cbb0f84 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -61,8 +61,9 @@ module ActiveRecord # # => "id ASC" def sanitize_sql_for_order(condition) if condition.is_a?(Array) && condition.first.to_s.include?("?") - disallow_raw_sql!([condition.first], - permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER + disallow_raw_sql!( + [condition.first], + permit: connection.column_name_with_order_matcher ) # Ensure we aren't dealing with a subclass of String that might @@ -133,6 +134,33 @@ module ActiveRecord end end + def disallow_raw_sql!(args, permit: connection.column_name_matcher) # :nodoc: + unexpected = nil + args.each do |arg| + next if arg.is_a?(Symbol) || Arel.arel_node?(arg) || permit.match?(arg.to_s) + (unexpected ||= []) << arg + end + + return unless unexpected + + if allow_unsafe_raw_sql == :deprecated + ActiveSupport::Deprecation.warn( + "Dangerous query method (method whose arguments are used as raw " \ + "SQL) called with non-attribute argument(s): " \ + "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \ + "arguments will be disallowed in Rails 6.1. This method should " \ + "not be called with user-provided values, such as request " \ + "parameters or model attributes. Known-safe values can be passed " \ + "by wrapping them in Arel.sql()." + ) + else + raise(ActiveRecord::UnknownAttributeReference, + "Query method called with non-attribute argument(s): " + + unexpected.map(&:inspect).join(", ") + ) + end + end + private def replace_bind_variables(statement, values) raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size) diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 76bf53387d..aba25fb375 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -50,7 +50,7 @@ module ActiveRecord instance_eval(&block) if info[:version].present? - ActiveRecord::SchemaMigration.create_table + connection.schema_migration.create_table connection.assume_migrated_upto_version(info[:version]) end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 2f7cc07221..f4b1f536b3 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -146,7 +146,11 @@ HEADER raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) next if column.name == pk type, colspec = column_spec(column) - tbl.print " t.#{type} #{column.name.inspect}" + if type.is_a?(Symbol) + tbl.print " t.#{type} #{column.name.inspect}" + else + tbl.print " t.column #{column.name.inspect}, #{type.inspect}" + end tbl.print ", #{format_colspec(colspec)}" if colspec.present? tbl.puts end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 74547de862..dec7fee986 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -22,10 +22,6 @@ module ActiveRecord "#{table_name_prefix}#{schema_migrations_table_name}#{table_name_suffix}" end - def table_exists? - connection.table_exists?(table_name) - end - def create_table unless table_exists? version_options = connection.internal_string_options_for_primary_key diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 35e9dcbffc..62c7988bd8 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -95,7 +95,6 @@ module ActiveRecord end private - def raise_invalid_scope_type!(scope_type) if !VALID_SCOPE_TYPES.include?(scope_type) raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES" diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 87bcfd5181..151eef362b 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -44,7 +44,6 @@ module ActiveRecord end private - # Use this macro in your model to set a default scope for all operations on # the model. # diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index cd9801b7a0..7baef99e83 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -204,7 +204,6 @@ module ActiveRecord end private - def valid_scope_name?(name) if respond_to?(name, true) && logger logger.warn "Creating scope :#{name}. " \ diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index b67479fb6a..9a1176db6a 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -4,17 +4,18 @@ module ActiveRecord class TableMetadata # :nodoc: delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true - def initialize(klass, arel_table, association = nil) + def initialize(klass, arel_table, association = nil, types = klass) @klass = klass + @types = types @arel_table = arel_table @association = association end def resolve_column_aliases(hash) new_hash = hash.dup - hash.each do |key, _| - if (key.is_a?(Symbol)) && klass.attribute_alias?(key) - new_hash[klass.attribute_alias(key)] = new_hash.delete(key) + hash.each_key do |key| + if key.is_a?(Symbol) && new_key = klass.attribute_aliases[key.to_s] + new_hash[new_key] = new_hash.delete(key) end end new_hash @@ -29,11 +30,7 @@ module ActiveRecord end def type(column_name) - if klass - klass.type_for_attribute(column_name) - else - Type.default_value - end + types.type_for_attribute(column_name) end def has_column?(column_name) @@ -52,13 +49,12 @@ module ActiveRecord elsif association && !association.polymorphic? association_klass = association.klass arel_table = association_klass.arel_table.alias(table_name) + TableMetadata.new(association_klass, arel_table, association) else type_caster = TypeCaster::Connection.new(klass, table_name) - association_klass = nil arel_table = Arel::Table.new(table_name, type_caster: type_caster) + TableMetadata.new(nil, arel_table, association, type_caster) end - - TableMetadata.new(association_klass, arel_table, association) end def polymorphic_association? @@ -74,6 +70,6 @@ module ActiveRecord end private - attr_reader :klass, :arel_table, :association + attr_reader :klass, :types, :arel_table, :association end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 7285c15477..5d1ce19829 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -141,8 +141,21 @@ module ActiveRecord end end - def for_each - databases = Rails.application.config.load_database_yaml + def setup_initial_database_yaml + return {} unless defined?(Rails) + + begin + Rails.application.config.load_database_yaml + rescue + $stderr.puts "Rails couldn't infer whether you are using multiple databases from your database.yml and can't generate the tasks for the non-primary databases. If you'd like to use this feature, please simplify your ERB." + + {} + end + end + + def for_each(databases) + return {} unless defined?(Rails) + database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env) # if this is a single database application we don't want tasks for each primary database @@ -153,8 +166,22 @@ module ActiveRecord end end - def create_current(environment = env) - each_current_configuration(environment) { |configuration| + def raise_for_multi_db(environment = env, command:) + db_configs = ActiveRecord::Base.configurations.configs_for(env_name: environment) + + if db_configs.count > 1 + dbs_list = [] + + db_configs.each do |db| + dbs_list << "#{command}:#{db.spec_name}" + end + + raise "You're using a multiple database application. To use `#{command}` you must run the namespaced task with a VERSION. Available tasks are #{dbs_list.to_sentence}." + end + end + + def create_current(environment = env, spec_name = nil) + each_current_configuration(environment, spec_name) { |configuration| create configuration } ActiveRecord::Base.establish_connection(environment.to_sym) @@ -184,9 +211,10 @@ module ActiveRecord def truncate_tables(configuration) ActiveRecord::Base.connected_to(database: { truncation: configuration }) do - table_names = ActiveRecord::Base.connection.tables + conn = ActiveRecord::Base.connection + table_names = conn.tables table_names -= [ - SchemaMigration.table_name, + conn.schema_migration.table_name, InternalMetadata.table_name ] @@ -217,7 +245,7 @@ module ActiveRecord end def migrate_status - unless ActiveRecord::SchemaMigration.table_exists? + unless ActiveRecord::Base.connection.schema_migration.table_exists? Kernel.abort "Schema migrations table does not exist yet." end @@ -309,6 +337,27 @@ module ActiveRecord Migration.verbose = verbose_was end + def dump_schema(configuration, format = ActiveRecord::Base.schema_format, spec_name = "primary") # :nodoc: + require "active_record/schema_dumper" + filename = dump_filename(spec_name, format) + connection = ActiveRecord::Base.connection + + case format + when :ruby + File.open(filename, "w:utf-8") do |file| + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + end + when :sql + structure_dump(configuration, filename) + if connection.schema_migration.table_exists? + File.open(filename, "a") do |f| + f.puts connection.dump_schema_information + f.print "\n" + end + end + end + end + def schema_file(format = ActiveRecord::Base.schema_format) File.join(db_dir, schema_file_type(format)) end @@ -390,12 +439,14 @@ module ActiveRecord task.is_a?(String) ? task.constantize : task end - def each_current_configuration(environment) + def each_current_configuration(environment, spec_name = nil) environments = [environment] environments << "test" if environment == "development" environments.each do |env| ActiveRecord::Base.configurations.configs_for(env_name: env).each do |db_config| + next if spec_name && spec_name != db_config.spec_name + yield db_config.config, db_config.spec_name, env end end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 1c1b29b5e1..e3efeb75b5 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -3,6 +3,8 @@ module ActiveRecord module Tasks # :nodoc: class MySQLDatabaseTasks # :nodoc: + ER_DB_CREATE_EXISTS = 1007 + delegate :connection, :establish_connection, to: ActiveRecord::Base def initialize(configuration) @@ -14,7 +16,7 @@ module ActiveRecord connection.create_database configuration["database"], creation_options establish_connection configuration rescue ActiveRecord::StatementInvalid => error - if error.message.include?("database exists") + if connection.error_number(error.cause) == ER_DB_CREATE_EXISTS raise DatabaseAlreadyExists else raise @@ -67,7 +69,6 @@ module ActiveRecord end private - attr_reader :configuration def configuration_without_database diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 8acb11f75f..626ffdfdf9 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -89,7 +89,6 @@ module ActiveRecord end private - attr_reader :configuration def encoding diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index a82cea80ca..f67a3498b6 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -59,7 +59,6 @@ module ActiveRecord end private - attr_reader :configuration, :root def run_cmd(cmd, args, out) diff --git a/activerecord/lib/active_record/test_fixtures.rb b/activerecord/lib/active_record/test_fixtures.rb index 8c60d71669..1d6fef1eb9 100644 --- a/activerecord/lib/active_record/test_fixtures.rb +++ b/activerecord/lib/active_record/test_fixtures.rb @@ -179,7 +179,6 @@ module ActiveRecord end private - # Shares the writing connection pool with connections on # other handlers. # diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 04a1c03474..c883d368b5 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -59,19 +59,26 @@ module ActiveRecord attribute_names.index_with(time || current_time_from_proper_timezone) end - private - def timestamp_attributes_for_create_in_model - timestamp_attributes_for_create.select { |c| column_names.include?(c) } - end + def timestamp_attributes_for_create_in_model + @timestamp_attributes_for_create_in_model ||= + (timestamp_attributes_for_create & column_names).freeze + end - def timestamp_attributes_for_update_in_model - timestamp_attributes_for_update.select { |c| column_names.include?(c) } - end + def timestamp_attributes_for_update_in_model + @timestamp_attributes_for_update_in_model ||= + (timestamp_attributes_for_update & column_names).freeze + end - def all_timestamp_attributes_in_model - timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model - end + def all_timestamp_attributes_in_model + @all_timestamp_attributes_in_model ||= + (timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model).freeze + end + + def current_time_from_proper_timezone + default_timezone == :utc ? Time.now.utc : Time.now + end + private def timestamp_attributes_for_create ["created_at", "created_on"] end @@ -80,13 +87,15 @@ module ActiveRecord ["updated_at", "updated_on"] end - def current_time_from_proper_timezone - default_timezone == :utc ? Time.now.utc : Time.now + def reload_schema_from_cache + @timestamp_attributes_for_create_in_model = nil + @timestamp_attributes_for_update_in_model = nil + @all_timestamp_attributes_in_model = nil + super end end private - def _create_record if record_timestamps current_time = current_time_from_proper_timezone @@ -124,19 +133,19 @@ module ActiveRecord end def timestamp_attributes_for_create_in_model - self.class.send(:timestamp_attributes_for_create_in_model) + self.class.timestamp_attributes_for_create_in_model end def timestamp_attributes_for_update_in_model - self.class.send(:timestamp_attributes_for_update_in_model) + self.class.timestamp_attributes_for_update_in_model end def all_timestamp_attributes_in_model - self.class.send(:all_timestamp_attributes_in_model) + self.class.all_timestamp_attributes_in_model end def current_time_from_proper_timezone - self.class.send(:current_time_from_proper_timezone) + self.class.current_time_from_proper_timezone end def max_updated_column_timestamp diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb index 980e42664b..3981bd46ad 100644 --- a/activerecord/lib/active_record/touch_later.rb +++ b/activerecord/lib/active_record/touch_later.rb @@ -10,12 +10,7 @@ module ActiveRecord end def touch_later(*names) # :nodoc: - unless persisted? - raise ActiveRecordError, <<-MSG.squish - cannot touch on a new or destroyed record object. Consider using - persisted?, new_record?, or destroyed? before touching - MSG - end + _raise_record_not_touched_error unless persisted? @_defer_touch_attrs ||= timestamp_attributes_for_update_in_model @_defer_touch_attrs |= names @@ -23,6 +18,7 @@ module ActiveRecord surreptitiously_touch @_defer_touch_attrs add_to_transaction + @_new_record_before_last_commit ||= false # touch the parents as we are not calling the after_save callbacks self.class.reflect_on_all_associations(:belongs_to).each do |r| @@ -40,7 +36,6 @@ module ActiveRecord end private - def surreptitiously_touch(attrs) attrs.each { |attr| write_attribute attr, @_touch_time } clear_attribute_changes attrs @@ -48,6 +43,7 @@ module ActiveRecord def touch_deferred_attributes if has_defer_touch_attrs? && persisted? + @_skip_dirty_tracking = true touch(*@_defer_touch_attrs, time: @_touch_time) @_defer_touch_attrs, @_touch_time = nil, nil end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 03a373f0af..5113e08e8e 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -164,12 +164,12 @@ module ActiveRecord # end # end # - # only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it. + # only "Kotori" is created. # # Most databases don't support true nested transactions. At the time of # writing, the only database that we're aware of that supports true nested # transactions, is MS-SQL. Because of this, Active Record emulates nested - # transactions by using savepoints on MySQL and PostgreSQL. See + # transactions by using savepoints. See # https://dev.mysql.com/doc/refman/5.7/en/savepoint.html # for more information about savepoints. # @@ -282,7 +282,6 @@ module ActiveRecord end private - def set_options_for_callbacks!(args, enforced_options = {}) options = args.extract_options!.merge!(enforced_options) args << options @@ -333,7 +332,7 @@ module ActiveRecord # Ensure that it is not called if the object was never persisted (failed create), # but call it after the commit of a destroyed object. def committed!(should_run_callbacks: true) #:nodoc: - if should_run_callbacks && (destroyed? || persisted?) + if should_run_callbacks @_committed_already_called = true _run_commit_without_transaction_enrollment_callbacks _run_commit_callbacks @@ -364,39 +363,40 @@ module ActiveRecord def with_transaction_returning_status status = nil self.class.transaction do - unless has_transactional_callbacks? - sync_with_transaction_state + if has_transactional_callbacks? + add_to_transaction + else + sync_with_transaction_state if @transaction_state&.finalized? @transaction_state = self.class.connection.transaction_state end remember_transaction_record_state status = yield raise ActiveRecord::Rollback unless status - ensure - if has_transactional_callbacks? && - (@_new_record_before_last_commit && !new_record? || _trigger_update_callback || _trigger_destroy_callback) - add_to_transaction - end end status end + def trigger_transactional_callbacks? # :nodoc: + (@_new_record_before_last_commit || _trigger_update_callback) && persisted? || + _trigger_destroy_callback && destroyed? + end + private attr_reader :_committed_already_called, :_trigger_update_callback, :_trigger_destroy_callback # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state - @_start_transaction_state.reverse_merge!( + @_start_transaction_state ||= { id: id, new_record: @new_record, destroyed: @destroyed, + attributes: @attributes, frozen?: frozen?, - ) - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - remember_new_record_before_last_commit - end + level: 0 + } + @_start_transaction_state[:level] += 1 - def remember_new_record_before_last_commit if _committed_already_called @_new_record_before_last_commit = false else @@ -406,28 +406,32 @@ module ActiveRecord # Clear the new record state and id of a record. def clear_transaction_record_state - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + return unless @_start_transaction_state + @_start_transaction_state[:level] -= 1 force_clear_transaction_record_state if @_start_transaction_state[:level] < 1 end # Force to clear the transaction record state. def force_clear_transaction_record_state - @_start_transaction_state.clear + @_start_transaction_state = nil @transaction_state = nil end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. def restore_transaction_record_state(force_restore_state = false) - unless @_start_transaction_state.empty? - transaction_level = (@_start_transaction_state[:level] || 0) - 1 - if transaction_level < 1 || force_restore_state - restore_state = @_start_transaction_state - thaw + if restore_state = @_start_transaction_state + if force_restore_state || restore_state[:level] <= 1 @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] - pk = self.class.primary_key - if pk && _read_attribute(pk) != restore_state[:id] - _write_attribute(pk, restore_state[:id]) + @attributes = restore_state[:attributes].map do |attr| + value = @attributes.fetch_value(attr.name) + attr = attr.with_value_from_user(value) if attr.value != value + attr + end + @mutations_from_database = nil + @mutations_before_last_save = nil + if @attributes.fetch_value(@primary_key) != restore_state[:id] + @attributes.write_from_user(@primary_key, restore_state[:id]) end freeze if restore_state[:frozen?] end @@ -472,7 +476,7 @@ module ActiveRecord # the TransactionState, and rolls back or commits the Active Record object # as appropriate. def sync_with_transaction_state - if (transaction_state = @transaction_state)&.finalized? + if transaction_state = @transaction_state if transaction_state.fully_committed? force_clear_transaction_record_state elsif transaction_state.committed? diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 03d00006b7..4c1ef1a7e4 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -47,7 +47,6 @@ module ActiveRecord end private - def current_adapter_name ActiveRecord::Base.connection.adapter_name.downcase.to_sym end diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb index b300fdfa05..c8c16635b1 100644 --- a/activerecord/lib/active_record/type/adapter_specific_registry.rb +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -11,7 +11,6 @@ module ActiveRecord end private - def registration_klass Registration end @@ -53,7 +52,6 @@ module ActiveRecord end protected - attr_reader :name, :block, :adapter, :override def priority @@ -72,7 +70,6 @@ module ActiveRecord end private - def matches_adapter?(adapter: nil, **) (self.adapter.nil? || adapter == self.adapter) end diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb index db9853fbcc..b260464df5 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -16,7 +16,6 @@ module ActiveRecord end private - def perform_fetch(type, *args, &block) @mapping.fetch(type, block).call(type, *args) end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 0a2f6cb9fb..a34b2fe702 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -56,7 +56,6 @@ module ActiveRecord end private - def default_value?(value) value == coder.load(nil) end diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index fc40b460f0..58f25ba075 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -45,7 +45,6 @@ module ActiveRecord end private - def perform_fetch(lookup_key, *args) matching_pair = @mapping.reverse_each.detect do |key, _| key === lookup_key diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb index 4619528f81..535369e630 100644 --- a/activerecord/lib/active_record/type/unsigned_integer.rb +++ b/activerecord/lib/active_record/type/unsigned_integer.rb @@ -4,7 +4,6 @@ module ActiveRecord module Type class UnsignedInteger < ActiveModel::Type::Integer # :nodoc: private - def max_value super * 2 end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 7cf8181d8e..f43559f4cb 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -8,21 +8,27 @@ module ActiveRecord @table_name = table_name end - def type_cast_for_database(attribute_name, value) + def type_cast_for_database(attr_name, value) return value if value.is_a?(Arel::Nodes::BindParam) - column = column_for(attribute_name) - connection.type_cast_from_column(column, value) + type = type_for_attribute(attr_name) + type.serialize(value) end - private - attr_reader :table_name - delegate :connection, to: :@klass + def type_for_attribute(attr_name) + schema_cache = connection.schema_cache - def column_for(attribute_name) - if connection.schema_cache.data_source_exists?(table_name) - connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] - end + if schema_cache.data_source_exists?(table_name) + column = schema_cache.columns_hash(table_name)[attr_name.to_s] + type = connection.lookup_cast_type_from_column(column) if column end + + type || Type.default_value + end + + delegate :connection, to: :@klass, private: true + + private + attr_reader :table_name end end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index ca27a3f0ab..23e8d53168 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -71,7 +71,6 @@ module ActiveRecord alias_method :validate, :valid? private - def default_validation_context new_record? ? :create : :update end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 3538aeec22..dc89df4be7 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -10,7 +10,6 @@ module ActiveRecord end private - def valid_object?(record) (record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid? end diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb index 361cd915cc..0fc07e1ede 100644 --- a/activerecord/lib/arel.rb +++ b/activerecord/lib/arel.rb @@ -12,7 +12,7 @@ require "arel/math" require "arel/alias_predication" require "arel/order_predications" require "arel/table" -require "arel/attributes" +require "arel/attributes/attribute" require "arel/visitors" require "arel/collectors/sql_string" diff --git a/activerecord/lib/arel/attributes.rb b/activerecord/lib/arel/attributes.rb deleted file mode 100644 index 35d586c948..0000000000 --- a/activerecord/lib/arel/attributes.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require "arel/attributes/attribute" - -module Arel # :nodoc: all - module Attributes - ### - # Factory method to wrap a raw database +column+ to an Arel Attribute. - def self.for(column) - case column.type - when :string, :text, :binary then String - when :integer then Integer - when :float then Float - when :decimal then Decimal - when :date, :datetime, :timestamp, :time then Time - when :boolean then Boolean - else - Undefined - end - end - end -end diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb index 8086102bde..0416ff58de 100644 --- a/activerecord/lib/arel/nodes/node.rb +++ b/activerecord/lib/arel/nodes/node.rb @@ -6,7 +6,6 @@ module Arel # :nodoc: all # Abstract base class for all AST nodes class Node include Arel::FactoryMethods - include Enumerable ### # Factory method to create a Nodes::Not node that has the recipient of @@ -38,13 +37,6 @@ module Arel # :nodoc: all collector = engine.connection.visitor.accept self, collector collector.value end - - # Iterate through AST, nodes will be yielded depth-first - def each(&block) - return enum_for(:each) unless block_given? - - ::Arel::Visitors::DepthFirst.new(block).accept self - end end end end diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb index 7dafde4952..895d394363 100644 --- a/activerecord/lib/arel/predications.rb +++ b/activerecord/lib/arel/predications.rb @@ -37,7 +37,7 @@ module Arel # :nodoc: all def between(other) if unboundable?(other.begin) == 1 || unboundable?(other.end) == -1 self.in([]) - elsif open_ended?(other.begin) + elsif other.begin.nil? || open_ended?(other.begin) if other.end.nil? || open_ended?(other.end) not_in([]) elsif other.exclude_end? @@ -85,7 +85,7 @@ Passing a range to `#in` is deprecated. Call `#between`, instead. def not_between(other) if unboundable?(other.begin) == 1 || unboundable?(other.end) == -1 not_in([]) - elsif open_ended?(other.begin) + elsif other.begin.nil? || open_ended?(other.begin) if other.end.nil? || open_ended?(other.end) self.in([]) elsif other.exclude_end? @@ -221,7 +221,6 @@ Passing a range to `#not_in` is deprecated. Call `#not_between`, instead. end private - def grouping_any(method_id, others, *extras) nodes = others.map { |expr| send(method_id, expr, *extras) } Nodes::Grouping.new nodes.inject { |memo, node| diff --git a/activerecord/lib/arel/visitors.rb b/activerecord/lib/arel/visitors.rb index e350f52e65..a1097f6750 100644 --- a/activerecord/lib/arel/visitors.rb +++ b/activerecord/lib/arel/visitors.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "arel/visitors/visitor" -require "arel/visitors/depth_first" require "arel/visitors/to_sql" require "arel/visitors/sqlite" require "arel/visitors/postgresql" diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb deleted file mode 100644 index d696edc507..0000000000 --- a/activerecord/lib/arel/visitors/depth_first.rb +++ /dev/null @@ -1,204 +0,0 @@ -# frozen_string_literal: true - -module Arel # :nodoc: all - module Visitors - class DepthFirst < Arel::Visitors::Visitor - def initialize(block = nil) - @block = block || Proc.new - super() - end - - private - - def visit(o) - super - @block.call o - end - - def unary(o) - visit o.expr - end - alias :visit_Arel_Nodes_Else :unary - alias :visit_Arel_Nodes_Group :unary - alias :visit_Arel_Nodes_Cube :unary - alias :visit_Arel_Nodes_RollUp :unary - alias :visit_Arel_Nodes_GroupingSet :unary - alias :visit_Arel_Nodes_GroupingElement :unary - alias :visit_Arel_Nodes_Grouping :unary - alias :visit_Arel_Nodes_Having :unary - alias :visit_Arel_Nodes_Lateral :unary - alias :visit_Arel_Nodes_Limit :unary - alias :visit_Arel_Nodes_Not :unary - alias :visit_Arel_Nodes_Offset :unary - alias :visit_Arel_Nodes_On :unary - alias :visit_Arel_Nodes_Ordering :unary - alias :visit_Arel_Nodes_Ascending :unary - alias :visit_Arel_Nodes_Descending :unary - alias :visit_Arel_Nodes_UnqualifiedColumn :unary - alias :visit_Arel_Nodes_OptimizerHints :unary - alias :visit_Arel_Nodes_ValuesList :unary - - def function(o) - visit o.expressions - visit o.alias - visit o.distinct - end - alias :visit_Arel_Nodes_Avg :function - alias :visit_Arel_Nodes_Exists :function - alias :visit_Arel_Nodes_Max :function - alias :visit_Arel_Nodes_Min :function - alias :visit_Arel_Nodes_Sum :function - - def visit_Arel_Nodes_NamedFunction(o) - visit o.name - visit o.expressions - visit o.distinct - visit o.alias - end - - def visit_Arel_Nodes_Count(o) - visit o.expressions - visit o.alias - visit o.distinct - end - - def visit_Arel_Nodes_Case(o) - visit o.case - visit o.conditions - visit o.default - end - - def nary(o) - o.children.each { |child| visit child } - end - alias :visit_Arel_Nodes_And :nary - - def binary(o) - visit o.left - visit o.right - end - alias :visit_Arel_Nodes_As :binary - alias :visit_Arel_Nodes_Assignment :binary - alias :visit_Arel_Nodes_Between :binary - alias :visit_Arel_Nodes_Concat :binary - alias :visit_Arel_Nodes_DeleteStatement :binary - alias :visit_Arel_Nodes_DoesNotMatch :binary - alias :visit_Arel_Nodes_Equality :binary - alias :visit_Arel_Nodes_FullOuterJoin :binary - alias :visit_Arel_Nodes_GreaterThan :binary - alias :visit_Arel_Nodes_GreaterThanOrEqual :binary - alias :visit_Arel_Nodes_In :binary - alias :visit_Arel_Nodes_InfixOperation :binary - alias :visit_Arel_Nodes_JoinSource :binary - alias :visit_Arel_Nodes_InnerJoin :binary - alias :visit_Arel_Nodes_LessThan :binary - alias :visit_Arel_Nodes_LessThanOrEqual :binary - alias :visit_Arel_Nodes_Matches :binary - alias :visit_Arel_Nodes_NotEqual :binary - alias :visit_Arel_Nodes_NotIn :binary - alias :visit_Arel_Nodes_NotRegexp :binary - alias :visit_Arel_Nodes_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 - alias :visit_Arel_Nodes_RightOuterJoin :binary - alias :visit_Arel_Nodes_TableAlias :binary - alias :visit_Arel_Nodes_When :binary - - def visit_Arel_Nodes_StringJoin(o) - visit o.left - end - - def visit_Arel_Attribute(o) - visit o.relation - visit o.name - end - alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute - alias :visit_Arel_Attributes_Float :visit_Arel_Attribute - alias :visit_Arel_Attributes_String :visit_Arel_Attribute - alias :visit_Arel_Attributes_Time :visit_Arel_Attribute - alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute - alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute - alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute - - def visit_Arel_Table(o) - visit o.name - end - - def terminal(o) - end - alias :visit_ActiveSupport_Multibyte_Chars :terminal - alias :visit_ActiveSupport_StringInquirer :terminal - alias :visit_Arel_Nodes_Lock :terminal - alias :visit_Arel_Nodes_Node :terminal - alias :visit_Arel_Nodes_SqlLiteral :terminal - alias :visit_Arel_Nodes_BindParam :terminal - alias :visit_Arel_Nodes_Window :terminal - alias :visit_Arel_Nodes_True :terminal - alias :visit_Arel_Nodes_False :terminal - alias :visit_BigDecimal :terminal - alias :visit_Class :terminal - alias :visit_Date :terminal - alias :visit_DateTime :terminal - alias :visit_FalseClass :terminal - alias :visit_Float :terminal - alias :visit_Integer :terminal - alias :visit_NilClass :terminal - alias :visit_String :terminal - alias :visit_Symbol :terminal - alias :visit_Time :terminal - alias :visit_TrueClass :terminal - - def visit_Arel_Nodes_InsertStatement(o) - visit o.relation - visit o.columns - visit o.values - end - - def visit_Arel_Nodes_SelectCore(o) - visit o.projections - visit o.source - visit o.wheres - visit o.groups - visit o.windows - visit o.havings - end - - def visit_Arel_Nodes_SelectStatement(o) - visit o.cores - visit o.orders - visit o.limit - visit o.lock - visit o.offset - end - - def visit_Arel_Nodes_UpdateStatement(o) - visit o.relation - visit o.values - visit o.wheres - visit o.orders - visit o.limit - end - - def visit_Arel_Nodes_Comment(o) - visit o.values - end - - def visit_Array(o) - o.each { |i| visit i } - end - alias :visit_Set :visit_Array - - def visit_Hash(o) - o.each { |k, v| visit(k); visit(v) } - end - - DISPATCH = dispatch_cache - - def get_dispatch_cache - DISPATCH - end - end - end -end diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb index ecc386de07..c4ea07bcfe 100644 --- a/activerecord/lib/arel/visitors/dot.rb +++ b/activerecord/lib/arel/visitors/dot.rb @@ -31,7 +31,6 @@ module Arel # :nodoc: all end private - def visit_Arel_Nodes_Ordering(o) visit_edge o, "expr" end diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb index 8475139870..92eb94f802 100644 --- a/activerecord/lib/arel/visitors/mssql.rb +++ b/activerecord/lib/arel/visitors/mssql.rb @@ -11,7 +11,6 @@ module Arel # :nodoc: all end private - def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) right = o.right diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb index 500974dff5..aab66301ef 100644 --- a/activerecord/lib/arel/visitors/oracle.rb +++ b/activerecord/lib/arel/visitors/oracle.rb @@ -4,7 +4,6 @@ module Arel # :nodoc: all module Visitors class Oracle < Arel::Visitors::ToSql private - def visit_Arel_Nodes_SelectStatement(o, collector) o = order_hacks(o) @@ -87,50 +86,6 @@ module Arel # :nodoc: all collector << " )" 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 - first = true - o.right.each_slice(in_clause_length) do |sliced_o_right| - collector << " OR " unless first - first = false - - collector = visit o.left, collector - collector << " IN (" - visit(sliced_o_right, collector) - collector << ")" - end - end - collector - 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 - first = true - o.right.each_slice(in_clause_length) do |sliced_o_right| - collector << " AND " unless first - first = false - - collector = visit o.left, collector - collector << " NOT IN (" - visit(sliced_o_right, collector) - collector << ")" - end - end - collector - end - def visit_Arel_Nodes_UpdateStatement(o, collector) # Oracle does not allow ORDER BY/LIMIT in UPDATEs. if o.orders.any? && o.limit.nil? @@ -198,10 +153,6 @@ module Arel # :nodoc: all collector = visit [o.left, o.right, 0, 1], collector collector << ")" end - - def in_clause_length - 1000 - end end end end diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb index 8e0f07fca9..36783243b5 100644 --- a/activerecord/lib/arel/visitors/oracle12.rb +++ b/activerecord/lib/arel/visitors/oracle12.rb @@ -4,15 +4,13 @@ module Arel # :nodoc: all module Visitors class Oracle12 < Arel::Visitors::ToSql private - def visit_Arel_Nodes_SelectStatement(o, collector) # Oracle does not allow LIMIT clause with select for update if o.limit && o.lock - raise ArgumentError, <<-MSG - 'Combination of limit and lock is not supported. - because generated SQL statements - `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014.` - MSG + raise ArgumentError, <<~MSG + Combination of limit and lock is not supported. Because generated SQL statements + `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014. + MSG end super end @@ -41,50 +39,6 @@ module Arel # :nodoc: all collector << " )" 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 - first = true - o.right.each_slice(in_clause_length) do |sliced_o_right| - collector << " OR " unless first - first = false - - collector = visit o.left, collector - collector << " IN (" - visit(sliced_o_right, collector) - collector << ")" - end - end - collector - 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 - first = true - o.right.each_slice(in_clause_length) do |sliced_o_right| - collector << " AND " unless first - first = false - - collector = visit o.left, collector - collector << " NOT IN (" - visit(sliced_o_right, collector) - collector << ")" - end - end - collector - end - def visit_Arel_Nodes_UpdateStatement(o, collector) # Oracle does not allow ORDER BY/LIMIT in UPDATEs. if o.orders.any? && o.limit.nil? @@ -106,10 +60,6 @@ module Arel # :nodoc: all collector = visit [o.left, o.right, 0, 1], collector collector << ")" end - - def in_clause_length - 1000 - end end end end diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb index 8296f1cdc1..d4f21ff93e 100644 --- a/activerecord/lib/arel/visitors/postgresql.rb +++ b/activerecord/lib/arel/visitors/postgresql.rb @@ -4,7 +4,6 @@ module Arel # :nodoc: all module Visitors class PostgreSQL < Arel::Visitors::ToSql private - def visit_Arel_Nodes_Matches(o, collector) op = o.case_sensitive ? " LIKE " : " ILIKE " collector = infix_value o, collector, op diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb index af6f7e856a..62ec74ad82 100644 --- a/activerecord/lib/arel/visitors/sqlite.rb +++ b/activerecord/lib/arel/visitors/sqlite.rb @@ -4,7 +4,6 @@ module Arel # :nodoc: all module Visitors class SQLite < Arel::Visitors::ToSql private - # Locks are not supported in SQLite def visit_Arel_Nodes_Lock(o, collector) collector diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 277d553e6c..eff7a0d036 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -19,7 +19,6 @@ module Arel # :nodoc: all end private - def visit_Arel_Nodes_DeleteStatement(o, collector) o = prepare_delete_statement(o) @@ -52,10 +51,14 @@ module Arel # :nodoc: all def visit_Arel_Nodes_InsertStatement(o, collector) collector << "INSERT INTO " collector = visit o.relation, collector - if o.columns.any? - collector << " (#{o.columns.map { |x| - quote_column_name x.name - }.join ', '})" + + unless o.columns.empty? + collector << " (" + o.columns.each_with_index do |x, i| + collector << ", " unless i == 0 + collector << quote_column_name(x.name) + end + collector << ")" end if o.values @@ -97,22 +100,20 @@ module Arel # :nodoc: all def visit_Arel_Nodes_ValuesList(o, collector) collector << "VALUES " - len = o.rows.length - 1 - o.rows.each_with_index { |row, i| + o.rows.each_with_index do |row, i| + collector << ", " unless i == 0 collector << "(" - row_len = row.length - 1 row.each_with_index do |value, k| + collector << ", " unless k == 0 case value when Nodes::SqlLiteral, Nodes::BindParam collector = visit(value, collector) else collector << quote(value).to_s end - collector << ", " unless k == row_len end collector << ")" - collector << ", " unless i == len - } + end collector end @@ -128,11 +129,10 @@ module Arel # :nodoc: all unless o.orders.empty? collector << " ORDER BY " - len = o.orders.length - 1 - o.orders.each_with_index { |x, i| + o.orders.each_with_index do |x, i| + collector << ", " unless i == 0 collector = visit(x, collector) - collector << ", " unless len == i - } + end end visit_Arel_Nodes_SelectOptions(o, collector) @@ -506,41 +506,73 @@ module Arel # :nodoc: all def visit_Arel_Table(o, collector) if o.table_alias - collector << "#{quote_table_name o.name} #{quote_table_name o.table_alias}" + collector << quote_table_name(o.name) << " " << quote_table_name(o.table_alias) else collector << quote_table_name(o.name) end end def visit_Arel_Nodes_In(o, collector) - if Array === o.right && !o.right.empty? + unless Array === o.right + return collect_in_clause(o.left, o.right, collector) + end + + unless o.right.empty? o.right.delete_if { |value| unboundable?(value) } end - if Array === o.right && o.right.empty? - collector << "1=0" + return collector << "1=0" if o.right.empty? + + in_clause_length = @connection.in_clause_length + + if !in_clause_length || o.right.length <= in_clause_length + collect_in_clause(o.left, o.right, collector) else - collector = visit o.left, collector - collector << " IN (" - visit(o.right, collector) << ")" + collector << "(" + o.right.each_slice(in_clause_length).each_with_index do |right, i| + collector << " OR " unless i == 0 + collect_in_clause(o.left, right, collector) + end + collector << ")" end end + def collect_in_clause(left, right, collector) + collector = visit left, collector + collector << " IN (" + visit(right, collector) << ")" + end + def visit_Arel_Nodes_NotIn(o, collector) - if Array === o.right && !o.right.empty? + unless Array === o.right + return collect_not_in_clause(o.left, o.right, collector) + end + + unless o.right.empty? o.right.delete_if { |value| unboundable?(value) } end - if Array === o.right && o.right.empty? - collector << "1=1" + return collector << "1=1" if o.right.empty? + + in_clause_length = @connection.in_clause_length + + if !in_clause_length || o.right.length <= in_clause_length + collect_not_in_clause(o.left, o.right, collector) else - collector = visit o.left, collector - collector << " NOT IN (" - collector = visit o.right, collector - collector << ")" + o.right.each_slice(in_clause_length).each_with_index do |right, i| + collector << " AND " unless i == 0 + collect_not_in_clause(o.left, right, collector) + end + collector end end + def collect_not_in_clause(left, right, collector) + collector = visit left, collector + collector << " NOT IN (" + visit(right, collector) << ")" + end + def visit_Arel_Nodes_And(o, collector) inject_join o.children, collector, " AND " end @@ -650,20 +682,13 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_UnqualifiedColumn(o, collector) - collector << "#{quote_column_name o.name}" - collector + collector << quote_column_name(o.name) end def visit_Arel_Attributes_Attribute(o, collector) join_name = o.relation.table_alias || o.relation.name - collector << "#{quote_table_name join_name}.#{quote_column_name o.name}" + collector << quote_table_name(join_name) << "." << quote_column_name(o.name) end - alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute - alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute - alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute - alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute - alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute - alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute def literal(o, collector); collector << o.to_s; end @@ -753,14 +778,11 @@ module Arel # :nodoc: all end def inject_join(list, collector, join_str) - len = list.length - 1 - list.each_with_index.inject(collector) { |c, (x, i)| - if i == len - visit x, c - else - visit(x, c) << join_str - end - } + list.each_with_index do |x, i| + collector << join_str unless i == 0 + collector = visit(x, collector) + end + collector end def unboundable?(value) diff --git a/activerecord/lib/arel/visitors/visitor.rb b/activerecord/lib/arel/visitors/visitor.rb index 1c17184e86..9066307aed 100644 --- a/activerecord/lib/arel/visitors/visitor.rb +++ b/activerecord/lib/arel/visitors/visitor.rb @@ -7,16 +7,15 @@ module Arel # :nodoc: all @dispatch = get_dispatch_cache end - def accept(object, *args) - visit object, *args + def accept(object, collector = nil) + visit object, collector end private - attr_reader :dispatch def self.dispatch_cache - Hash.new do |hash, klass| + @dispatch_cache ||= Hash.new do |hash, klass| hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}" end end @@ -25,9 +24,13 @@ module Arel # :nodoc: all self.class.dispatch_cache end - def visit(object, *args) + def visit(object, collector = nil) dispatch_method = dispatch[object.class] - send dispatch_method, object, *args + if collector + send dispatch_method, object, collector + else + send dispatch_method, object + end rescue NoMethodError => e raise e if respond_to?(dispatch_method, true) superklass = object.class.ancestors.find { |klass| diff --git a/activerecord/lib/arel/visitors/where_sql.rb b/activerecord/lib/arel/visitors/where_sql.rb index c6caf5e7c9..8fb299d1c8 100644 --- a/activerecord/lib/arel/visitors/where_sql.rb +++ b/activerecord/lib/arel/visitors/where_sql.rb @@ -9,7 +9,6 @@ module Arel # :nodoc: all end private - def visit_Arel_Nodes_SelectCore(o, collector) collector << "WHERE " wheres = o.wheres.map do |where| diff --git a/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb b/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb index 35d5664400..56b9628a92 100644 --- a/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb +++ b/activerecord/lib/rails/generators/active_record/application_record/application_record_generator.rb @@ -13,7 +13,6 @@ module ActiveRecord end private - def application_record_file_name @application_record_file_name ||= if namespaced? diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb index cbb88d571d..af753071a9 100644 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -17,7 +17,6 @@ module ActiveRecord end private - def primary_key_type key_type = options[:primary_key_type] ", id: :#{key_type}" if key_type diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index cb2c74f1ca..0620a515bd 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -7,6 +7,7 @@ module ActiveRecord class MigrationGenerator < Base # :nodoc: argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]" + class_option :timestamps, type: :boolean class_option :primary_key_type, type: :string, desc: "The type for primary key" class_option :database, type: :string, aliases: %i(--db), desc: "The database for your migration. By default, the current environment's primary database is used." diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index c71bbdcab8..d4733f948f 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -35,7 +35,6 @@ module ActiveRecord hook_for :test_framework private - def attributes_with_index attributes.select { |a| !a.reference? && a.has_index? } end |