diff options
Diffstat (limited to 'activerecord/lib/active_record')
123 files changed, 759 insertions, 412 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/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/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 85e0f076da..0db0ad8595 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -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/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 342d9e7a5a..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 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 220043c061..21f72bb6c7 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -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.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 - # 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,10 +409,6 @@ module ActiveRecord end end - def readonly_attribute?(name) - self.class.readonly_attributes.include?(name) - end - def pk_attribute?(name) name == @primary_key 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 3d917ec9b1..4a7b6c60e5 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -66,7 +66,6 @@ module ActiveRecord 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) diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index a43ebdf60d..45341765c1 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -177,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) @@ -193,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 b4f5e6e75a..768c5f8c05 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -45,7 +45,6 @@ module ActiveRecord end private - def attribute_method?(attr_name) attr_name == "id" || super end @@ -120,7 +119,6 @@ module ActiveRecord 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 0562327a9a..0f0e721b24 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -7,7 +7,6 @@ module ActiveRecord module ClassMethods # :nodoc: private - def define_method_attribute(name) ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( generated_attribute_methods, name 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 1c63b553d0..66536a8ddf 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -11,7 +11,6 @@ module ActiveRecord module ClassMethods # :nodoc: private - def define_method_attribute=(name) ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( generated_attribute_methods, name, writer: true, 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 2d4f0ad77e..94d8134b55 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. 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..9b3f5260f7 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, @@ -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: @@ -809,7 +871,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 +999,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 +1140,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_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..6fec4dbd81 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -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/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 2b64e96450..79dc98607a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -291,25 +291,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 +323,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 +501,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 +521,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 +739,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 # @@ -1060,8 +1064,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 @@ -1077,7 +1081,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) @@ -1450,7 +1454,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 7b6321d83b..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,6 +100,7 @@ module ActiveRecord end def rollback_records + return unless records ite = records.uniq(&:object_id) already_run_callbacks = {} while record = ite.shift @@ -107,16 +110,17 @@ module ActiveRecord 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 + return unless records ite = records.uniq(&:object_id) already_run_callbacks = {} while record = ite.shift @@ -131,7 +135,7 @@ module ActiveRecord 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 @@ -141,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) @@ -202,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! @@ -274,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 @@ -309,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 bf0bb84c93..c0ead17b3b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -78,7 +78,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! @@ -114,11 +114,9 @@ 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 +142,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,7 +150,22 @@ 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 @@ -206,9 +207,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 @@ -487,6 +492,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 @@ -587,7 +595,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 d239ecff89..70292e6d63 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 @@ -176,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 @@ -298,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 @@ -318,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 @@ -522,7 +515,6 @@ module ActiveRecord end private - def initialize_type_map(m = type_map) super @@ -804,7 +796,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..df26f67c6e 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -56,7 +56,6 @@ module ActiveRecord 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..dfed5471f4 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -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/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 5b0335c22b..53510c62c2 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -109,12 +109,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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index ec25bb1e19..b4ea6c42d3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -23,6 +23,19 @@ module ActiveRecord def sql_type super.sub(/\[\]\z/, "") 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/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/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..0c800dca83 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -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..0062952667 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 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..6b18a12bce 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -302,6 +302,7 @@ module ActiveRecord end def discard! # :nodoc: + super @connection.socket_io.reopen(IO::NULL) rescue nil @connection = nil end @@ -452,7 +453,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..7d54fcf9a0 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,12 +36,13 @@ 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) @@ -79,9 +79,7 @@ module ActiveRecord # 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[table_name] ||= columns(table_name).index_by(&:name) end # Checks whether the columns hash is already cached for a table. @@ -124,15 +122,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 73b80e8c15..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}" @@ -79,7 +84,6 @@ module ActiveRecord 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..58787cf9db 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -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..da971fdba7 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: # @@ -201,14 +201,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 +218,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 +383,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 +480,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 040ebdb960..c8cefa9906 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -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 dfd33d3dd7..595ef4ee25 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -286,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) } @@ -554,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, 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 398a029068..7d9e221faa 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -69,7 +69,6 @@ module ActiveRecord end private - def body "#{finder}(#{attributes_hash})" end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 60cf9818c1..c8c06375a3 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 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/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/integration.rb b/activerecord/lib/active_record/integration.rb index 573a823dbc..4a97061731 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -93,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 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 6711ee9bf4..c2a083bf3b 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -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/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb index 93a1a39c3e..7374107048 100644 --- a/activerecord/lib/active_record/middleware/database_selector.rb +++ b/activerecord/lib/active_record/middleware/database_selector.rb @@ -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 f20edbeb93..7edfec9903 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -494,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 @@ -568,7 +568,6 @@ module ActiveRecord end private - def connection ActiveRecord::Base.connection end @@ -885,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) @@ -992,7 +992,6 @@ module ActiveRecord delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration private - def migration @migration ||= load_migration end @@ -1014,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) @@ -1048,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) @@ -1058,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 @@ -1108,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 @@ -1153,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) @@ -1172,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 @@ -1246,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 } @@ -1330,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/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..cab2369b71 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -354,7 +354,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 +385,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 a58c5fd48a..323b01ab2d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -865,7 +865,6 @@ module ActiveRecord end private - # A hook to be overridden by association modules. def destroy_associations end @@ -939,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 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 e0bc5180c0..d17acc408c 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -250,7 +250,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:'}" @@ -261,17 +265,56 @@ db_namespace = namespace :db do end end + namespace :abort_if_pending_migrations do + ActiveRecord::Tasks::DatabaseTasks.for_each 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 + 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 + + ActiveRecord::Tasks::DatabaseTasks.migrate + + # Skipped when no database + ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, ActiveRecord::Base.schema_format, db_config.spec_name) + 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" @@ -336,13 +379,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: ActiveRecord::Tasks::DatabaseTasks.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.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, :ruby, db_config.spec_name) end db_namespace["schema:dump"].reenable @@ -385,14 +424,7 @@ db_namespace = namespace :db do task dump: :load_config do 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 eefda2b8f4..cbfa60d4d9 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -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 add95f6a0a..ea8f44752b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -468,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 = {} 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/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index d59331053e..2f61c05eca 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -99,7 +99,6 @@ module ActiveRecord end private - def method_missing(method, *args, &block) if @klass.respond_to?(method) @klass.generate_relation_method(method) @@ -116,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 9c7ac80447..6deb9c7da8 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 diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 84fe424ef0..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? diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index b8fd2fce14..87a53966e4 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1128,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].delete_if(&: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_type) + join_sources = manager.join_sources + join_sources.concat(join_nodes) unless join_nodes.empty? - joins = join_dependency.join_constraints(stashed_joins, 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) @@ -1165,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 @@ -1253,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 @@ -1260,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) @@ -1273,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 @@ -1295,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. 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 b91b135867..8fae380b0a 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -87,7 +87,6 @@ module ActiveRecord end protected - attr_reader :predicates def referenced_columns 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_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 073866b894..9a1176db6a 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -4,8 +4,9 @@ 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 @@ -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 c79ed8db60..aecc9350e8 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -169,8 +169,8 @@ module ActiveRecord end end - def create_current(environment = env) - each_current_configuration(environment) { |configuration| + def create_current(environment = env, spec_name = nil) + each_current_configuration(environment, spec_name) { |configuration| create configuration } ActiveRecord::Base.establish_connection(environment.to_sym) @@ -200,9 +200,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 ] @@ -233,7 +234,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 @@ -325,6 +326,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 @@ -406,12 +428,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..0d9917a4db 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -67,7 +67,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 6872b7844e..3981bd46ad 100644 --- a/activerecord/lib/active_record/touch_later.rb +++ b/activerecord/lib/active_record/touch_later.rb @@ -36,7 +36,6 @@ module ActiveRecord end private - def surreptitiously_touch(attrs) attrs.each { |attr| write_attribute attr, @_touch_time } clear_attribute_changes attrs @@ -44,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 148bc0550c..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 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 |